Библиотеку, работающую с блокчейном, назовём как «blockchain», а
исходный файл под названием «blockchain.go» расположим в директории
«blockchain/».
——————————————————————————————————
package blockchain
——————————————————————————————————
Приступать к написанию библиотеки необходимо с создания структур, на
основе которых будут базироваться все последующие действия. Блокчейн даёт
три основные структуры - BlockChain, Block, Transaction.
Структура BlockChain.
——————————————————————————————————
type BlockChain struct {
DB
*sql.DB
}
——————————————————————————————————
Хранит в себе указатель на базу данных, в которую добавляет и из
которой берёт блоки.
Структура Block.
——————————————————————————————————
type Block struct {
Nonce uint64
Difficulty uint8
CurrHash []byte
PrevHash []byte
Transactions []Transaction
Mapping map[string]uint64
Miner string
Signature []byte
TimeStamp
string
}
——————————————————————————————————
Содержит следующие поля:
Difficulty (сложность блока) и Nonce (результат подтверждения
работы);CurrHash (хеш текущего блока) и PrevHash (хеш предыдущего
блока);Transactions (транзакции пользователей) и Mapping (состояния
баланса пользователей);Miner (пользователь замайнивший блок) и Signature (подпись
майнера указывающая на CurrHash);TimeStamp (метка времени создания блока);
CurrHash является результатом хеширования следующих значений:
Difficulty, PrevHash, Transactions, Mapping, Miner и TimeStamp. Signature и
Nonce привязаны к CurrHash.Структура Transaction.
——————————————————————————————————
type Transaction struct {
RandBytes []byte
PrevBlock []byte
Sender string
Receiver string
Value uint64
ToStorage uint64
CurrHash []byte
Signature []byte
}
——————————————————————————————————
Состоит из следующих полей:
RandBytes (случайные байты), PrevBlock (хеш последнего блока в
блокчейне);Sender (имя отправителя), Receiver (имя получателя), Value
(количество переводимых денег получателю);ToStorage (количество переводимых денег хранилищу);
CurrHash (хеш текущей транзакции), Signature (подпись
отправителя);
CurrHash является результатом хеширования всех полей, кроме Signature.
Определив основные структуры данных, можно приступать к написанию
функций. В первую очередь нужно реализовать функцию создания блокчейна
NewChain.Функция NewChain.
——————————————————————————————————
func NewChain(filename, receiver string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
file.Close()
db, err := sql.Open("sqlite3", filename)
if err != nil {
return err
}
defer db.Close()
_, err = db.Exec(CREATE_TABLE)
chain := &BlockChain{
DB: db,
}
genesis := &Block{
PrevHash: []byte(GENESIS_BLOCK),
Mapping: make(map[string]uint64),
Miner:
receiver,
TimeStamp: time.Now().Format(time.RFC3339),
}
genesis.Mapping[STORAGE_CHAIN] = STORAGE_VALUE
genesis.Mapping[receiver] = GENESIS_REWARD
genesis.CurrHash = genesis.hash()
chain.AddBlock(genesis)
return nil
}
——————————————————————————————————
Принимает в качестве первого аргумента имя файла, в качестве второго -
имя пользователя в блокчейне (в контексте генезис-блока, имя майнера).
Возвращает либо нулевой адрес как выполнение без ошибки, либо возвращает
строку как обнаружение ошибки. Сначала функция создаёт файл при помощи
функции os.Create, далее открывает этот файл как локальную БД, при помощи
функции sql.Open. Исполняет команду создания таблицы, которая прописана в
константе CREATE_TABLE. Далее создаётся переменная типа BlockChain,
в которую заносится указатель на БД, и генезис-блок, в котором
прописывается майнер, а также время создания. После этого в состояние
генезис-блока заносятся балансы пользователей (хранилища и
создателя). И в конце, блок добавляется в БД при помощи метода AddBlock.
Константа CREATE_TABLE.
——————————————————————————————————
const (
CREATE_TABLE = CREATE TABLE BlockChain ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Hash VARCHAR(44) UNIQUE, Block TEXT );
)
——————————————————————————————————
Константа GENESIS_BLOCK, STORAGE_VALUE, GENESIS_REWARD, STORAGE_CHAIN.
——————————————————————————————————
const (
GENESIS_BLOCK = "GENESIS-BLOCK"
STORAGE_VALUE = 100
GENESIS_REWARD = 100
STORAGE_CHAIN = "STORAGE-CHAIN"
)
——————————————————————————————————
Метод AddBlock.
——————————————————————————————————
func (chain *BlockChain) AddBlock(block *Block) {
chain.DB.Exec("INSERT INTO BlockChain (Hash, Block) VALUES ($1, $2)",
Base64Encode(block.CurrHash),
SerializeBlock(block),
)
}
——————————————————————————————————
Добавляет новый блок в БД. Использует функции Base64Encode и
SerializeBlock
Функция Base64Encode.
——————————————————————————————————
func Base64Encode(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
——————————————————————————————————
Функция SerializeBlock.
——————————————————————————————————
func SerializeBlock(block * Block) string {
jsonData, err := json.MarshalIndent(*block, "", "\t")
if err != nil {
return ""
}
return string(jsonData)
}
——————————————————————————————————
Чтобы воспользоваться уже созданным блокчейном нужно создать
функцию подгрузки БД LoadChain.
Функция LoadChain.
——————————————————————————————————
func LoadChain(filename string) *BlockChain {
db, err := sql.Open("sqlite3", filename)
if err != nil {
return nil
}
chain := &BlockChain{
DB: db,
}
return chain
}
——————————————————————————————————
Принимает в качестве аргумента имя файла и возвращает экземпляр
структуры BlockChain. Открывает локальное БД.
Чтобы продолжать далее писать код связанный с блокчейном ,
необходимым является написание функций, связанных с действиями над
блоками. Самой основной функцией является создание блока.
Функция NewBlock.
——————————————————————————————————
func NewBlock(miner string, prevHash []byte) *Block {
return &Block{
Difficulty: DIFFICULTY,
PrevHash: prevHash,
Miner:
Mapping:
miner,
make(map[string]uint64),
}
}
——————————————————————————————————
Создаёт шаблон блока. Принимает в качестве аргументов имя майнера и
хеш предыдущего блока. Использует также константу DIFFICULTY.
Константность сложности обусловлена лёгкостью воспроизведения кода, без
оглядки на саморегулирование сложности цепи.
Константа DIFFICULTY.
——————————————————————————————————
const (
DIFFICULTY = 20
)
——————————————————————————————————
Значение константы равно количеству первых нулевых бит в хеше.
После создания блока, в него нужно вносить транзакции пользователей.
Чтобы создать транзакцию, надлежит использовать функцию NewTransaction.
Функция NewTransaction.
——————————————————————————————————
func NewTransaction(user *User, lasthash []byte, to string, value uint64) *Transaction {
tx := &Transaction{
RandBytes: GenerateRandomBytes(RAND_BYTES),
PrevBlock: lasthash,
Sender:
user.Address(),
Receiver: to,
Value:
value,
}
if value > START_PERCENT {
tx.ToStorage = STORAGE_REWARD
}
tx.CurrHash = tx.hash()
tx.Signature = tx.sign(user.Private())
return tx
}
——————————————————————————————————
Принимает в качестве аргументов объект структуры User, хеш
последнего блока в блокчейне, имя получателя и количество отправляемых
средств. Сама функция создаёт транзакцию, проверяет количество
отправляемых средств и если они превышают константу START_PERCENT,
тогда из кошелька берутся дополнительные деньги в размере
STORAGE_REWARD, которые перенаправятся в хранилище. В данном коде
появляются новые элементы, ранее неизвестные, такие как функция
GenerateRandomBytes, константы RAND_BYTES,
START_PERCENT, STORAGE_REWARD и методы Address,
Private, hash, sign.
Структура User.
——————————————————————————————————
type User struct {
PrivateKey *rsa.PrivateKey
}
——————————————————————————————————
Представляет из себя лишь оболочку над типом данных rsa.PrivateKey.
Функция GenerateRandomBytes.
——————————————————————————————————
func GenerateRandomBytes(max uint) []byte {
var slice []byte = make([]byte, max)
_, err := rand.Read(slice)
if err != nil {
return nil
}
return slice
}
——————————————————————————————————
Принимает в качестве аргумента число, как количество необходимых
байт для генерации. Возвращает срез из псевдослучайных байт.
Константа RAND_BYTES, START_PERCENT, STORAGE_REWARD.
——————————————————————————————————
const (
RAND_BYTES = 32
START_PERCENT = 10
STORAGE_REWARD = 1
)
——————————————————————————————————
Метод Address.
——————————————————————————————————
func (user *User) Address() string {
return StringPublic(user.Public())
}
——————————————————————————————————
Преобразует публичный ключ в строку. Использует функцию StringPublic
и метод Public.
Метод Private.
——————————————————————————————————
func (user *User) Private() *rsa.PrivateKey {
return user.PrivateKey
}
——————————————————————————————————
Метод hash.
——————————————————————————————————
func (tx *Transaction) hash() []byte {
return HashSum(bytes.Join(
[][]byte{
tx.RandBytes,
tx.PrevBlock,
[]byte(tx.Sender),
[]byte(tx.Receiver),
ToBytes(tx.Value),
ToBytes(tx.ToStorage),
},
[]byte{},
))
}
——————————————————————————————————
Конкатенирует байты полей объекта, после чего производит над
получившимся значением хеширование. Использует функции HashSum и
ToBytes.
Метод sign. Использует функцию Sign.
——————————————————————————————————
func (tx *Transaction) sign(priv *rsa.PrivateKey) []byte {
return Sign(priv, tx.CurrHash)
}
——————————————————————————————————
Функция StringPublic.
——————————————————————————————————
func StringPublic(pub *rsa.PublicKey) string {
return Base64Encode(x509.MarshalPKCS1PublicKey(pub))
}
——————————————————————————————————
Переводит публичный ключ в набор байтов, после чего применяет
кодировку base64 для трансляции в строку.
Метод Public.
——————————————————————————————————
func (user *User) Public() *rsa.PublicKey {
return &(user.PrivateKey).PublicKey
}
——————————————————————————————————
Функция HashSum.
——————————————————————————————————
func HashSum(data []byte) []byte {
hash := sha256.Sum256(data)
return hash[:]
}
——————————————————————————————————
Функция ToBytes.
——————————————————————————————————
func ToBytes(num uint64) []byte {
var data = new(bytes.Buffer)
err := binary.Write(data, binary.BigEndian, num)
if err != nil {
return nil
}
return data.Bytes()
}
——————————————————————————————————
Переводит число в набор байтов.
Функция Sign.
——————————————————————————————————
func Sign(priv *rsa.PrivateKey, data []byte) []byte {
signature, err := rsa.SignPSS(rand.Reader, priv, crypto.SHA256, data, nil)
if err != nil {
return nil
}
return signature
}
——————————————————————————————————
Подписывает данные исходя из приватного ключа.
После создания транзакции её необходимо добавить в блок. Для этого
создадим метод AddTransaction.
Метод AddTransaction.
——————————————————————————————————
func (block *Block) AddTransaction(chain *BlockChain, tx *Transaction) error {
if tx == nil {
return errors.New("tx is null")
}
if tx.Value == 0 {
return errors.New("tx value = 0")
}
if tx.Sender != STORAGE_CHAIN && len(block.Transactions) == TXS_LIMIT {
return errors.New("len tx = limit")
}
if tx.Sender != STORAGE_CHAIN && tx.Value > START_PERCENT &&
tx.ToStorage != STORAGE_REWARD {
return errors.New("storage reward pass")
}
if !bytes.Equal(tx.PrevBlock, chain.LastHash()) {
return errors.New("prev block in tx /= last hash in chain")
}
var balanceInChain uint64
balanceInTX := tx.Value + tx.ToStorage
if value, ok := block.Mapping[tx.Sender]; ok {
balanceInChain = value
} else {
balanceInChain = chain.Balance(tx.Sender, chain.Size())
}
if balanceInTX > balanceInChain {
return errors.New("insufficient funds")
}
block.Mapping[tx.Sender] = balanceInChain - balanceInTX
block.addBalance(chain, tx.Receiver, tx.Value)
block.addBalance(chain, STORAGE_CHAIN, tx.ToStorage)
block.Transactions = append(block.Transactions, *tx)
return nil
}
——————————————————————————————————
Метод привязан к блоку, принимает в качестве аргументов объект типа
BlockChain и объект типа Transaction. Возвращает нулевой адрес, как результат
выполнения без ошибки, либо строку, как результат выполнения с ошибкой.
Данная функция сначала сравнивает транзакцию и отправляемые средства на
нуль, далее проверяет при помощи константы TXS_LIMIT
перегруженность блока транзакциями (блок имеет фиксированное количество
допустимых транзакций). После чего берётся баланс отправителя, при помощи
метода Balance или при помощи состояния, если в блоке уже существуют
транзакции от этого пользователя. Далее идёт сравнение отправляемых данных
с константами START_PERCENT, STORAGE_REWARD. И последнее
сравнение связано с указанным балансом в транзакции и общим балансом
пользователя по блокчейну. Если проверки оказались успешными (то-есть без
ошибок), тогда состояние отправителя обновляется, а после и состояния
получателя вместе с хранилищем . В конце, данная транзакция заносится
в список транзакций блока. Используется метод Size.
Константа TXS_LIMIT.
——————————————————————————————————
const (
TXS_LIMIT = 2
)
——————————————————————————————————
Определяет максимальное количество транзакций в одном блоке.
Метод Balance.
——————————————————————————————————
func (chain *BlockChain) Balance(address string, size uint64) uint64 {
var (
sblock string
block *Block
balance uint64
)
rows, err := chain.DB.Query("SELECT Block FROM BlockChain WHERE Id <= $1
ORDER BY Id DESC", size)
if err != nil {
return balance
}
defer rows.Close()
for rows.Next() {
rows.Scan(&sblock)
block = DeserializeBlock(sblock)
if value, ok := block.Mapping[address]; ok {
balance = value
break
}
}
return balance
}
——————————————————————————————————
Привязан к объекту структуры BlockChain. В качестве аргумента
принимает имя пользователя. Возвращает баланс указанного пользователя.
Производит считывание блоков с конца цепочки, пока не найдёт состояние, в
котором имя пользователя будет указано. После нахождения весь дальнейший
поиск прекращается. При взятии блока из локальной БД используется функция
DeserializeBlock.
Метод addBalance.
——————————————————————————————————
func (block *Block) addBalance(chain *BlockChain, receiver string, value uint64) {
var balanceInChain uint64
if v, ok := block.Mapping[receiver]; ok {
balanceInChain = v
} else {
balanceInChain = chain.Balance(receiver, chain.Size())
}
block.Mapping[receiver] = balanceInChain + value
}
——————————————————————————————————
Привязан к объекту структуры Block. Принимает в качестве аргументов
объект типа BlockChain, получателя и переданные ему средства, после чего
производит обновление состояния.
Метод Size.
——————————————————————————————————
func (chain *BlockChain) Size() uint64 {
var size uint64
row := chain.DB.QueryRow("SELECT Id FROM BlockChain ORDER BY Id
DESC")
row.Scan(&size)
return size
}
——————————————————————————————————
Возвращает количество блоков в локальной БД.
Функция DeserializeBlock.
——————————————————————————————————
func DeserializeBlock(data string) *Block {
var block Block
err := json.Unmarshal([]byte(data), &block)
if err != nil {
return nil
}
return &block
}
——————————————————————————————————
Поместив все транзакции в блок, его необходимо подтвердить во-первых
подписью, во-вторых работой (PoW). Для этого создадим метод Accept.
Метод Accept.
——————————————————————————————————
func (block *Block) Accept(chain *BlockChain, user *User, ch chan bool) error {
if !block.transactionsIsValid(chain, chain.Size()) {
return errors.New("transactions is not valid")
}
block.AddTransaction(chain, &Transaction{
RandBytes: GenerateRandomBytes(RAND_BYTES),
PrevBlock: chain.LastHash(),
Sender:
STORAGE_CHAIN,
Receiver: user.Address(),
Value:
STORAGE_REWARD,
})
block.TimeStamp = time.Now().Format(time.RFC3339)
block.CurrHash = block.hash()
block.Signature = block.sign(user.Private())
block.Nonce = block.proof(ch)
return nil
}
——————————————————————————————————
Привязан к объекту структуры Block. Принимает в качестве аргументов
объект типа BlockChain, объект типа User и канал типа chan bool. Возвращает
нулевой адрес при успешном выполнении, либо строку, при обнаружении
ошибки. Канал (chan bool) необходим для прекращения выполнения PoW. Это
необходимо в моменты, когда другой узел найдёт подходящий хеш быстрее,
соответственно и замайнитсам блок, из чего следует, что нет смысла
продолжать дальше пытаться майнить этот хеш. Сама функция использует
метод transactionsIsValid для проверки правильности транзакций, которые
находятся в блоке. После чего использует функцию добавления транзакции от
имени хранилища, переводя награду майнеру. Если в хранилище не останется
средств, тогда метод AddTransaction выдаст ошибку и не добавит транзакцию в
блок. Далее на блок помещается метка времени (следует заметить, что метка
использует именно локальное время), вычисляется хеш , блок
подписывается и подтверждается работой.
Метод transactionsIsValid.
——————————————————————————————————
func (block *Block) transactionsIsValid(chain *BlockChain, size uint64) bool {
lentxs := len(block.Transactions)
plusStorage := 0
for i := 0; i < lentxs; i++ {
if block.Transactions[i].Sender == STORAGE_CHAIN {
plusStorage = 1
break
}
}
if lentxs == 0 || lentxs > TXS_LIMIT+plusStorage {
return false
}
for i := 0; i < lentxs-1; i++ {
for j := i + 1; j < lentxs; j++ {
if bytes.Equal(block.Transactions[i].RandBytes,
block.Transactions[j].RandBytes) {
return false
}
if
block.Transactions[i].Sender == STORAGE_CHAIN &&
block.Transactions[j].Sender == STORAGE_CHAIN {
return false
}
}
}
for i := 0; i < lentxs; i++ {
tx := block.Transactions[i]
if tx.Sender == STORAGE_CHAIN {
if tx.Receiver != block.Miner || tx.Value != STORAGE_REWARD {
return false
}
} else {
if !tx.hashIsValid() {
return false
}
if !tx.signIsValid() {
return false
}
}
if !block.balanceIsValid(chain, tx.Sender, size) {
return false
}
if !block.balanceIsValid(chain, tx.Receiver, size) {
return false
}
}
return true
}
——————————————————————————————————
Привязан к объекту структуры Block. В качестве аргумента принимает
объект типа BlockChain. Возвращает true, если все транзакции валидны, иначе
false. Сначала сравнивает лимит транзакций блока с количеством транзакций в
блоке. Далее проверяет не существует ли одинаковых случайных байт и не
существует ли больше чем одной транзакции от хранилища (в одном блоке
может существовать лишь одна транзакция, где отправителем является само
хранилище). Далее проверяется каждая отдельная транзакция, проверяется её
хеш, подпись отправителя, и сравниваются балансы отправителя и
получателя по состоянию блока при помощи метода balanceIsValid.
Метод hash.
——————————————————————————————————
func (block *Block) hash() []byte {
var tempHash []byte
for _, tx := range block.Transactions {
tempHash = 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 = HashSum(bytes.Join(
[][]byte{
tempHash,
[]byte(hash),
ToBytes(block.Mapping[hash]),
},
[]byte{},
))
}
return HashSum(bytes.Join(
[][]byte{
tempHash,
ToBytes(uint64(block.Difficulty)),
block.PrevHash,
[]byte(block.Miner),
[]byte(block.TimeStamp),
},
[]byte{},
))
}
——————————————————————————————————
Сначала поочерёдно хешируются все транзакции. Далее осуществляется
перевод хеш-таблицы в срез, после чего производится сортировка имён
пользователе (это необходимо делать, так как запись и последующее хранение в
хеш-таблицах могут отличаться друг от друга, тем самым приводя к получению
разных хешей). После сортировки, поочерёдно начинает хешироваться
значения из среза. И в конце, осуществляется хеширование всех других полей в
совокупности с транзакциями и состояниями.
Метод sign.
——————————————————————————————————
func (block *Block) sign(priv *rsa.PrivateKey) []byte {
return Sign(priv, block.CurrHash)
}
——————————————————————————————————
Метод proof. Использует функцию ProofOfWork.
——————————————————————————————————
func (block *Block) proof(ch chan bool) uint64 {
return ProofOfWork(block.CurrHash, block.Difficulty, ch)
}
——————————————————————————————————
Метод hashIsValid.
——————————————————————————————————
func (tx *Transaction) hashIsValid() bool {
return bytes.Equal(tx.hash(), tx.CurrHash)
}
——————————————————————————————————
Метод signIsValid.
——————————————————————————————————
func (tx *Transaction) signIsValid() bool {
return Verify(ParsePublic(tx.Sender), tx.CurrHash, tx.Signature) == nil
}
——————————————————————————————————
Переводит имя пользователя (которое является лишь публичным ключом
в строковом формате) в тип *rsa.PublicKey. Далее с этим ключом проверяется
правильность подписанного хеша. Использует функции Verify и
ParsePublic.
Метод balanceIsValid.
——————————————————————————————————
func (block *Block) balanceIsValid(chain *BlockChain, address string, size uint64) bool {
if _, ok := block.Mapping[address]; !ok {
return false
}
lentxs := len(block.Transactions)
balanceInChain := chain.Balance(address, size)
balanceSubBlock := uint64(0)
balanceAddBlock := uint64(0)
for j := 0; j < lentxs; j++ {
tx := block.Transactions[j]
if tx.Sender == address {
balanceSubBlock += tx.Value + tx.ToStorage
}
if tx.Receiver == address {
balanceAddBlock += tx.Value
}
if STORAGE_CHAIN == address {
balanceAddBlock += tx.ToStorage
}
}
if (balanceInChain + balanceAddBlock - balanceSubBlock) !=
block.Mapping[address] {
return false
}
return true
}
——————————————————————————————————
Проверяет совместимость данных, которые хранятся в транзакциях, с
данными, которые хранятся в состоянии по указанному имени пользователи.
Если существует какое-то различие, тогда возвращается значение false, как
результат того, что данные невалидны, иначе true. Правильность вычисляется
по формуле: balanceInChain + balanceAddBlock - balanceSubBlock. То-есть
сначала берётся баланс, хранимый в блокчейне, к нему прибавляются средства
полученные и вычитаются средства отправленные. Если полученное значение
не равняется значению, хранимому в состоянии, тогда выдаётся ошибка.
Функция ProofOfWork.
——————————————————————————————————
func ProofOfWork(blockHash []byte, difficulty uint8, ch chan bool) uint64 {
var (
Target = big.NewInt(1)
intHash = big.NewInt(1)
nonce = uint64(mrand.Intn(math.MaxUint32))
hash
[]byte
)
Target.Lsh(Target, 256 - uint(difficulty))
for nonce < math.MaxUint64 {
select {
case <-ch:
if DEBUG {
fmt.Println()
}
return nonce
default:
hash = HashSum(bytes.Join(
[][]byte{
blockHash,
ToBytes(nonce),
},
[]byte{},
))
if DEBUG {
fmt.Printf("\rMining: %s", Base64Encode(hash))
}
intHash.SetBytes(hash)
if intHash.Cmp(Target) == -1 {
if DEBUG {
fmt.Println()
}
return nonce
}
nonce++
}
}
return nonce
}
——————————————————————————————————
Создаёт локальную переменную Target, которая создана для сравнения с
получаемым хешем. Изначально Target равняется единице, что единице в
двоичном виде. Далее производится сдвиг влево на (256 - DIFFICULTY) битов.
Чем меньше DIFFICULTY, тем легче найти значение хеша. В nonce
устанавливается случайное число, в диапазоне [0;UINT32_MAX), во избежание
одних и тех же вычислений со стороны разных узлов (так как в данной
реализации не существует пул-серверов). Каждый раз nonce инкрементируется
на единицу, после чего конкатенируется с хешем блока и хешируется.
Полученный хеш преобразуется в число, после чего сравнивается с Target. И
если полученное число оказывается меньше Target’a, это говорит о том, что
работа подтверждена (нашёлся такой хеш, количество начальных битовых
нулей которого равно или больше указанного числа в DIFFICULTY).
Использует константу DEBUG. При этом стоит заметить, что число nonce
генерируется исходя из функции Intn (пакет math/rand), которое в свою очередь
использует seed при генерации псевдослучайной последовательности. По-
умолчанию seed является статичным и не изменяется, тем самым при запуске
приложения генерируемая последовательность будет всегда одинаковой. Чтобы
избавиться от этого, необходимо при каждом запуске приложения изменять
seed. В качестве seed’a можно ложить текущее время.
Функция Verify.
——————————————————————————————————
func Verify(pub *rsa.PublicKey, data, sign []byte) error {
return rsa.VerifyPSS(pub, crypto.SHA256, data, sign, nil)
}
——————————————————————————————————
Использует публичный ключ для проверки подписанных данных с
первоначальными.
Функция ParsePublic.
——————————————————————————————————
func ParsePublic(pubData string) *rsa.PublicKey {
pub, err := x509.ParsePKCS1PublicKey(Base64Decode(pubData))
if err != nil {
return nil
}
return pub
}
——————————————————————————————————
Использует функцию Base64Decode.
Константа DEBUG.
——————————————————————————————————
const (
DEBUG = true
)
——————————————————————————————————
Функция init.
——————————————————————————————————
func init() {
mrand.Seed(time.Now().UnixNano())
}
——————————————————————————————————
Изменяет seed генератора псевдослучайных чисел, из пакета math/rand,
методом вычисления текущего времени. Запустится автоматически, при
подключении пакета «blockchain».
Функция Base64Decode.
——————————————————————————————————
func Base64Decode(data string) []byte {
result, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return nil
}
return result
}
——————————————————————————————————
Осталось лишь реализовать дополнительные функции и методы:
генерация приватного ключа GeneratePrivate, перевод приватного ключа в
строку StringPrivate, перевод строки в приватный ключ ParsePrivate,
создать пользователя NewUser, загрузить пользователя LoadUser,
получить кошелёк Purse, получить последний хеш в блокчейне LastHash,
проверить блок IsValid, сериализовать транзакцию SerializeTX,
десериализовать транзакцию DeserializeTX.
Функция GeneratePrivate.
——————————————————————————————————
func GeneratePrivate(bits uint) *rsa.PrivateKey {
priv, err := rsa.GenerateKey(rand.Reader, int(bits))
if err != nil {
return nil
}
return priv
}
——————————————————————————————————
Функция StringPrivate.
——————————————————————————————————
func StringPrivate(priv *rsa.PrivateKey) string {
return Base64Encode(x509.MarshalPKCS1PrivateKey(priv))
}
——————————————————————————————————
Функция ParsePrivate.
——————————————————————————————————
func ParsePrivate(privData string) *rsa.PrivateKey {
priv , err := x509.ParsePKCS1PrivateKey(Base64Decode(privData))
if err != nil {
return nil
}
return priv
}
——————————————————————————————————
Функция NewUser.
——————————————————————————————————
func NewUser() *User {
return &User{
PrivateKey: GeneratePrivate(KEY_SIZE),
}
}
——————————————————————————————————
Использует константу KEY_SIZE).
Функция LoadUser.
——————————————————————————————————
func LoadUser(purse string) *User {
priv := ParsePrivate(purse)
if priv == nil {
return nil
}
return &User{
PrivateKey: priv,
}
}
——————————————————————————————————
Метод Purse.
——————————————————————————————————
func (user *User) Purse() string {
return StringPrivate(user.Private())
}
——————————————————————————————————
Метод LastHash.
——————————————————————————————————
func (chain *BlockChain) LastHash() []byte {
var hash string
row := chain.DB.QueryRow("SELECT Hash FROM BlockChain ORDER BY Id
DESC")
row.Scan(&hash)
return Base64Decode(hash)
}
——————————————————————————————————
Метод IsValid.
——————————————————————————————————
func (block *Block) IsValid(chain *BlockChain, size uint64) bool {
switch {
case block == nil:
return false
case block.Difficulty != DIFFICULTY:
return false
case !block.hashIsValid(chain, size):
return false
case !block.signIsValid():
return false
case !block.proofIsValid():
return false
case !block.mappingIsValid():
return false
case !block.timeIsValid(chain):
return false
case !block.transactionsIsValid(chain, size):
return false
}
return true
}
——————————————————————————————————
Использует методы hashIsValid, signIsValid, proofIsValid,
mappingIsValid, timeIsValid.
Функция SerializeTX.
——————————————————————————————————
func SerializeTX(tx * Transaction) string {
jsonData, err := json.MarshalIndent(*tx, "", "\t")
if err != nil {
return ""
}
return string(jsonData)
}
——————————————————————————————————
Функция DeserializeTX.
——————————————————————————————————
func DeserializeTX(data string) *Transaction {
var tx Transaction
err := json.Unmarshal([]byte(data), &tx)
if err != nil {
return nil
}
return &tx
}
——————————————————————————————————
Константа KEY_SIZE.
——————————————————————————————————
const (
KEY_SIZE = 512
)
——————————————————————————————————
Метод hashIsValid.
——————————————————————————————————
func (block *Block) hashIsValid(chain *BlockChain, size uint64) bool {
if !bytes.Equal(block.hash(), block.CurrHash) {
return false
}
var id uint64
row := chain.DB.QueryRow("SELECT Id FROM BlockChain WHERE Hash=$1",
Base64Encode(block.PrevHash))
row.Scan(&id)
return id == size
}
——————————————————————————————————
Проверяет хеш текущего блока пропущенного через метод hash с хешем,
хранимым в поле блоке. Также проверяет хеш предыдущего блока из блокчейна
с хешем, хранимым в поле блока методом получения его ID из БД. Возвращает
false при обнаружении ошибки, иначе true.
Метод signIsValid.
——————————————————————————————————
func (block *Block) signIsValid() bool {
return Verify(ParsePublic(block.Miner), block.CurrHash, block.Signature) == nil
}
——————————————————————————————————
Метод proofIsValid.
——————————————————————————————————
func (block *Block) proofIsValid() bool {
intHash := big.NewInt(1)
Target := big.NewInt(1)
hash := HashSum(bytes.Join(
[][]byte{
block.CurrHash,
ToBytes(block.Nonce),
},
[]byte{},
))
intHash.SetBytes(hash)
Target.Lsh(Target, 256 - uint(block.Difficulty))
if intHash.Cmp(Target) == -1 {
return true
}
return false
}
——————————————————————————————————
Проверяет правильность работы используя указанные в блоке поля Nonce
и CurrHash со сложностью Difficulty. Возвращает false, если работа не была
выполнена, иначе true.
Метод mappingIsValid.
——————————————————————————————————
func (block *Block) mappingIsValid() bool {
for hash := range block.Mapping {
if hash == STORAGE_CHAIN {
continue
}
flag := false
for _, tx := range block.Transactions {
if tx.Sender == hash || tx.Receiver == hash {
flag = true
break
}
}
if !flag {
return false
}
}
return true
}
——————————————————————————————————
Проверяет состояние блока на наличие пользователей, которые не
указаны в транзакциях. Если таковые имеются, тогда функция возвращает false,
как результат обнаружения ошибки, иначе true. Исключением является лишь
хранилище, так как ему передаются средства через поле ToStorage, неявной
транзакцией.
Метод timeIsValid.
——————————————————————————————————
func (block *Block) timeIsValid(chain *BlockChain) bool {
btime, err := time.Parse(time.RFC3339, block.TimeStamp)
if err != nil {
return false
}
diff := time.Now().Sub(btime)
if diff < 0 {
return false
}
var sblock string
row := chain.DB.QueryRow("SELECT Block FROM BlockChain WHERE
Hash=$1",
Base64Encode(block.PrevHash))
row.Scan(&sblock)
lblock := DeserializeBlock(sblock)
if lblock == nil {
return false
}
ltime, err := time.Parse(time.RFC3339, lblock.TimeStamp)
if err != nil {
return false
}
result := btime.Sub(ltime)
return result > 0
}
——————————————————————————————————
Первым шагом проверяется время, указанное в блоке, с текущим времен.
Если указанное время в блоке больше текущего, тогда возвращается false, как
результат ошибки. Далее проверяется время указанное в предыдущем блоке.
Если время предыдущего блока больше времени текущего блока, тогда также
возвращается false как результат с ошибкой, иначе true.
Данная библиотека использует следующие пакеты:
——————————————————————————————————
import (
"time"
"errors"
"bytes"
"sort"
"database/sql"
_ "github.com/mattn/go-sqlite3"
"os"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"math"
"math/big"
mrand "math/rand"
)
——————————————————————————————————
Написав всю библиотеку, можно проверить корректность её исполнения
на примере простой программы создания блоков. Этот код также будет
являться неплохим шаблоном для последующего его проецирования на
приложение узла.
——————————————————————————————————
package main
import (
"fmt"
bc "./blockchain"
)
const (
DBNAME = "blockchain.db"
)
func main() {
miner := bc.NewUser()
bc.NewChain(DBNAME, miner.Address())
chain := bc.LoadChain(DBNAME)
for i := 0; i < 3; i++ {
block := bc.NewBlock(miner.Address(), chain.LastHash())
block.AddTransaction(chain,
bc.NewTransaction(miner, chain.LastHash(), "aaa", 5))
block.AddTransaction(chain,
bc.NewTransaction(miner, chain.LastHash(), "bbb", 3))
block.Accept(chain, miner, make(chan bool))
chain.AddBlock(block)
}
var sblock string
rows, err := chain.DB.Query("SELECT Block FROM BlockChain")
if err != nil {
panic("error: query to db")
}
for rows.Next() {
rows.Scan(&sblock)
fmt.Println(sblock)
}
}
——————————————————————————————————
В данном коде создаётся майнер, при помощи функции NewUser, далее
он же указывается создателем блокчейна. Имея средства, он перенаправляет их
на другие адреса («aaa», «bbb») методом создания блока и занесения своих
транзакций в этот блок, после чего подтверждает блок и заносит его в
блокчейн. В конце программы у БД запрашиваются все блоки, которые она
хранит, после чего осуществляется их вывод.
Назовём данный файл как «main.go». Он должен располагаться рядом с
директорией «blockchain/».
——————————————————————————————————
blockchain/
blockchain.go
main.go
——————————————————————————————————
Чтобы скомпилировать и запустить этот код, необходимо прописать
следующие команды в терминале:
——————————————————————————————————
$ go build main.go
$ ./main
——————————————————————————————————
Результат работы (сам результат всегда будет различаться за счёт
использования разного времени и псевдослучайных данных, но структура
останется той же).
——————————————————————————————————
Mining: AAAO5I2yq5bVdlgc2fwTRt6EHyNtGx83wkknMTpssrI=
Mining: AAAJ5w65nEn2chP0+82Q95wpZGKvkSXtSJhvlNK6914=
Mining: AAABVxEyH8HP5ERLWTgJn0VFw2kpckNZI03nFW7lpnY=
...
{
"Nonce": 1958934804,
"Difficulty": 20,
"CurrHash": "qiZqMnhrAJqDK+iPdwc1O1sfKH0r9JgrzYLuolfphRQ=",
"PrevHash": "kckTOJ5tyLiDT+2bCkdk+exmFgRsXvZHo45FQTAPVmg=",
"Transactions": [
{
"RandBytes":
"Yn6OG6U4EEH5HRntgq39hfwgYKiAwk7rqg3M0D30TYs=",
"PrevBlock": "kckTOJ5tyLiDT+2bCkdk+exmFgRsXvZHo45FQTAPVmg=",
"Sender":
"MEgCQQDC7/4owVPVNI824jP4rQgtRSt4+GXTJXv99l6ue2LHQa9FbFbc7jTV7ipmJtRJ1kgcZ
Novx1ZH12w1QUdt70rpAgMBAAE=",
"Receiver": "aaa",
"Value": 5,
"ToStorage": 0,
"CurrHash": "+RT60qjzzpFNe+YHH5mR2WjNotLZLEBVkT5ajJl91AI=",
"Signature":
"hMW4fy0kGA9dDmOE7lq68BACsqJDUBqUMADay23R3E8jqmQpsfQ0wS94INPNDMmTk6g
FF58sT7ycXbDC3wWcgQ=="
},
{
"RandBytes": "OvqFb8DEVMMFo0CS4PL91txNG7Y1Co+jojip+n73OS8=",
"PrevBlock": "kckTOJ5tyLiDT+2bCkdk+exmFgRsXvZHo45FQTAPVmg=",
"Sender":
"MEgCQQDC7/4owVPVNI824jP4rQgtRSt4+GXTJXv99l6ue2LHQa9FbFbc7jTV7ipmJtRJ1kgcZ
Novx1ZH12w1QUdt70rpAgMBAAE=",
"Receiver": "bbb",
"Value": 3,
"ToStorage": 0,
"CurrHash": "3FDpgkXHehrqp/fyvNHge6H8qzbPuQaJzNXKfDuA734=",
"Signature":
"lr9UJ2d5NBjw/m4Vv6541xkFt/HTFK2PzKJflFSPO1JbSbBF1QqjW8Opa6U8HeXt4lHb+Bq3uxP
xHEWrUj0L+g=="
},
{
"RandBytes": "8slLSjJzWDPbLuAr6I/FdH0e24kVOq7ZyftAOgBarFE=",
"PrevBlock": null,
"Sender": "STORAGE-CHAIN",
"Receiver":
"MEgCQQDC7/4owVPVNI824jP4rQgtRSt4+GXTJXv99l6ue2LHQa9FbFbc7jTV7ipmJtRJ1kgcZ
Novx1ZH12w1QUdt70rpAgMBAAE=",
"Value": 1,
"ToStorage": 0,
"CurrHash": null,
"Signature": null
}
],
"Mapping": {
"MEgCQQDC7/4owVPVNI824jP4rQgtRSt4+GXTJXv99l6ue2LHQa9FbFbc7jTV7ipmJtRJ
1kgcZNovx1ZH12w1QUdt70rpAgMBAAE=": 79,
"STORAGE-CHAIN": 97,
"aaa": 15,
"bbb": 9
},
"Miner":
"MEgCQQDC7/4owVPVNI824jP4rQgtRSt4+GXTJXv99l6ue2LHQa9FbFbc7jTV7ipmJtRJ1kgcZ
Novx1ZH12w1QUdt70rpAgMBAAE=",
"Signature":
"fXcaZH/0jUV6eRKgnxWOXpQuJxw5ZB9B5fU8gHYybwamZ0srwEkC2TZadkgfPDrcBcVtfaA3
ndlS29vH/RhLwQ==",
"TimeStamp": "2020-07-17T06:38:39-04:00"
}
——————————————————————————————————
В результате работы программы показан момент с майнингом, а также
был сокращён весь список блоков до последнего. В нём можно увидеть
последнее состояние пользователей. Так например, майнер перевёл
пользователям «aaa» и «bbb» в совокупности 24 монеты, и у самого майнера их
осталось 79. При этом, у майнера до всех этих транзакций было 100 монет. 3
монеты взялись из хранилища и передались майнеру за проделанную работу. В
итоге, хранилище имеет 97 монет в своей «казне».
При этом, если бы в одной из транзакций было указано больше 10 монет,
тогда у хранилища запас «казны» пополнился бы на одну монету, за счёт её
взятия у майнера отправляющего свои монеты. В итоге, баланс хранилища
оставался бы постоянным (равным 100), а майнер, переводящий монеты,
выплачивал эту комиссию из майнинга (за счёт того, что в реализации, награда
за майнинг равна комиссии за транзакцию, при сумме большей 10).