Pull to refresh

Blockchain на Go. Часть 4: Транзакции, часть 1

Reading time 12 min
Views 17K
Привет, Habr! Представляю вашему вниманию перевод статьи "Building Blockchain in Go. Part 4: Transactions 1".

Содержание

  1. Blockchain на Go. Часть 1: Прототип
  2. Blockchain на Go. Часть 2: Proof-of-Work
  3. Blockchain на Go. Часть 3: Постоянная память и интерфейс командной строки
  4. Blockchain на Go. Часть 4: Транзакции, часть 1
  5. Blockchain на Go. Часть 5: Адреса
  6. Blockchain на Go. Часть 6: Транзакции, часть 2
  7. Blockchain на Go. Часть 7: Сеть

Вступление


Транзакции являются сердцем Биткоина, и единственная цель цепи блоков — это хранить транзакции безопасным и надежным способом, чтобы никто не смог модифицировать их после создания. В этой статье мы начинаем работу над реализацией механизма транзакций. Но поскольку это довольно большая тема, я разбил ее на две части: в этой части мы реализуем общий механизм, а во второй части мы детально разберем весь остальной функционал.

В данной статье мы будем практически полностью редактировать весь наш предыдущий код, поэтому не имеет смысла описывать каждое изменение, все изменения вы можете посмотреть тут.

Ложки нет


Если Вы когда-то разрабатывали веб-приложение, то для реализации платежей, вероятно, создавали в базе данных две эти таблицы: учетные записи и транзакции. Учетная запись хранила информацию о пользователе, включая его персональную информацию и баланс, а транзакция хранит информацию о переводе денег с одной учетной записи на другую. В Биткоине платежи осуществляются совершенно по-другому:

  1. Нет аккаунтов.
  2. Нет балансов.
  3. Нет адресов.
  4. Нет монет.
  5. Нет отправителей и получателей.

Поскольку блокчейн является публичной и открытой базой данных, мы не хотим хранить конфиденциальную информацию о владельцах кошельков. Монеты не хранятся на счетах. Транзакции не переводят деньги с одного адреса на другой. Нет полей и атрибутов, которые содержат баланс счета. Есть только транзакции. Но что внутри?

Биткоин транзакция


Транзакция представляет собой комбинацию входов и выходов:

type Transaction struct {
	ID   []byte
	Vin  []TXInput
	Vout []TXOutput
}

Входы новой транзакции ссылаются на выходы предыдущей транзакции (есть исключение, о котором мы поговорим ниже). Выходы — место, где хранятся монеты. Следующая диаграмма демонстрирует взаимосвязь транзакций:



Заметьте:

  1. Есть выходы, которые не связаны с входами.
  2. В одной транзакции входы могут ссылаться на выходы нескольких транзакций.
  3. Вход всегда должен ссылаться на выход.

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

Выходы транзакций


Начнем с выходов:

type TXOutput struct {
	Value        int
	ScriptPubKey string
}

Фактически, это выходы, которые хранят «монеты» (обратите внимание на полеValue выше). Средства блокируются особым пазлом, которая хранится в ScriptPubKey. Внутри Bitcoin использует скриптовый язык Script, который используется для определения логики блокировки и разблокировки выходов. Язык довольно примитивен (это делается намеренно, чтобы избежать возможных взломов), но мы не будем обсуждать его подробно. Вы можете прочитать подробнее о нем здесь.
В Биткоине поле значений хранит количество сатоши, а не количество BTC. 1 сатоши = 0.00000001 BTC. Таким образом, это самая маленькая единица валюты в Биткойне (как, к примеру, цент).
Поскольку у нас нет адресов, пока что мы будем избегать всей связанной с сценариями логики. Для начала ScriptPubKey будет хранить произвольную строку (пользовательский адрес кошелька).
Кстати, наличие такого языка сценариев означает, что Биткоин можно использовать как платформу смарт-контрактов.
Одна важная вещь которую нужно знать о выходах, это то, что они неделимы, а это означает, что вы не можете ссылаться на часть своего значения. Когда выход ссылается на новую транзакцию, он расходуется полностью. И если его значение больше, чем требуется, генерируется разница и новое значение отправляется обратно отправителю. Это похоже на реальную ситуацию в мире, когда вы платите, скажем, $5 долларов за то, что стоит $1, и получаете сдачу $4.

Входы транзакций


Рассмотрим вход:

type TXInput struct {
	Txid      []byte
	Vout      int
	ScriptSig string
}

Как упоминалось ранее, вход ссылается на предыдущий выход: Txid хранит идентификатор такой транзакции, а Vout хранит индекс выхода данной транзакции. ScriptSig — это скрипт, который предоставляет данные, которые будут в дальнейшем использоваться в скрипте ScriptPubKey. Если данные верны, выход можно разблокировать, а его значение можно использовать для генерации новых выходов; если же это не так, вход не может ссылаться на выход. Этот механизм гарантирует, что пользователи не могут тратить монеты, принадлежащие другим людям.

Опять же, поскольку у нас все еще нет адресов, ScriptSig сохранит только произвольный пользовательский адрес кошелька. Мы создадим открытый ключ и проверку подписи в следующей статье.

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

Но что пришло первым: входы или выходы?

Яйцо


В Биткоине, яйцо появилось до цыпленка. Логика inputs-referencing-outputs представляет собой классическую ситуацию «курица или яйцо»: входы производят выходы, а выходы позволяют создавать входы. И в Биткоине выходы всегда появляются перед входами.

Когда майнер начинает майнить блок, он добавляет к нему coinbase транзакцию. Транзакция coinbase — это особый тип транзакции, который не требует ранее существующих выходов. Он создает выходы (т. е. «Монеты») из ниоткуда. Яйцо без курицы. Это награда, которую майнеры получают за добычу новых блоков.

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

Давайте создадим coinbase транзакцию:

func NewCoinbaseTX(to, data string) *Transaction {
	if data == "" {
		data = fmt.Sprintf("Reward to '%s'", to)
	}

	txin := TXInput{[]byte{}, -1, data}
	txout := TXOutput{subsidy, to}
	tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
	tx.SetID()

	return &tx
}

В coinbase транзакции имеется только один вход. В нашей реализации Txid пуст, а Vout равен -1. Кроме того, транзакция coinbase не хранит скрипт в ScriptSig. Вместо этого там хранятся произвольные данные.
В биткойне самая первая coinbase транзакция содержит следующее сообщение: “The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”. Вы сами можете на это посмотреть.
subsidy — это сумма вознаграждения. В Биткоине это число не хранится нигде и рассчитывается только на основе общего количества блоков: количество блоков делится на 210000. Майнинг блока генезиса приносит 50 BTC, и каждые 210000 блоков награда уменьшается вдвое. В нашей реализации мы будем хранить вознаграждение как константу (по крайней мере на данный момент).

Сохранение транзакций в цепи


С этого момента каждый блок должен хранить как минимум одну транзакцию, и должно стать невозможным майнить блоки без транзакции. Теперь, удалим поле date из Block и вместо этого теперь будем хранить транзакции.

type Block struct {
	Timestamp     int64
	Transactions  []*Transaction
	PrevBlockHash []byte
	Hash          []byte
	Nonce         int
}

NewBlock и NewGenesisBlock также должны быть соответствующим образом изменены

func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block {
	block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}
	...
}

func NewGenesisBlock(coinbase *Transaction) *Block {
	return NewBlock([]*Transaction{coinbase}, []byte{})
}

Теперь создадим функцию CreateBlockchain

func CreateBlockchain(address string) *Blockchain {
	...
	err = db.Update(func(tx *bolt.Tx) error {
		cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
		genesis := NewGenesisBlock(cbtx)

		b, err := tx.CreateBucket([]byte(blocksBucket))
		err = b.Put(genesis.Hash, genesis.Serialize())
		...
	})
	...
}

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

Proof-of-Work


Алгоритм Proof-of-Work должен рассматривать транзакции, хранящиеся в блоке, чтобы гарантировать согласованность и надежность цепи как хранилища транзакции. Итак, теперь мы должны изменить метод ProofOfWork.prepareData:

func (pow *ProofOfWork) prepareData(nonce int) []byte {
	data := bytes.Join(
		[][]byte{
			pow.block.PrevBlockHash,
			pow.block.HashTransactions(), // This line was changed
			IntToHex(pow.block.Timestamp),
			IntToHex(int64(targetBits)),
			IntToHex(int64(nonce)),
		},
		[]byte{},
	)

	return data
}

Вместо pow.block.Data теперь мы добавим pow.block.HashTransactions ():

func (b *Block) HashTransactions() []byte {
	var txHashes [][]byte
	var txHash [32]byte

	for _, tx := range b.Transactions {
		txHashes = append(txHashes, tx.ID)
	}
	txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))

	return txHash[:]
}

Повторюсь, мы используем хеширование как механизм обеспечения уникального представления данных. Мы хотим, чтобы все транзакции в блоке были однозначно идентифицированы с помощью одного хеша. Чтобы достичь этого, мы получаем хеши каждой транзакции, объединяем их и получаем хеш объединенных комбинаций.
Биткоин использует более сложную технику: он представляет все транзакции, содержащиеся в блоке как дерево хешей, и использует корневой хеш дерева в системе Proof-of-Work. Такой подход позволяет быстро проверить, содержит ли блок определенную транзакцию, имеющую только корневой хеш и без загрузки всех транзакций.
Проверим правильность работы:

$ blockchain_go createblockchain -address Ivan
00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8a

Done!

Отлично! Мы получили свою первую награду. Но как нам проверить баланс?

Непотраченные выходы


Нам нужно найти все непотраченные выходы (UTXO). Это означает, что эти выходы не ссылались ни на какие входы. На приведенной выше диаграмме это:

  1. tx0, output 1;
  2. tx1, output 0;
  3. tx3, output 0;
  4. tx4, output 0.

Конечно, когда мы проверяем баланс, нам не нужны они все, нужны только те, которые могут быть разблокированы ключом, которым мы владеем (в настоящее время у нас нет реализованных ключей и вместо них будут использоваться пользовательские адреса). Для начала, давайте определим методы блокировки-разблокировки на входах и выходах:

func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
	return in.ScriptSig == unlockingData
}

func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
	return out.ScriptPubKey == unlockingData
}

Здесь мы просто сравниваем поля из ScriptSig с unlockingData. В следующей статье мы их улучшим, после того, как реализуем адреса, основанные на закрытых ключах.

Следующий шаг — поиск транзакций с непотраченными выходами — это уже более сложно:

func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {
  var unspentTXs []Transaction
  spentTXOs := make(map[string][]int)
  bci := bc.Iterator()

  for {
    block := bci.Next()

    for _, tx := range block.Transactions {
      txID := hex.EncodeToString(tx.ID)

    Outputs:
      for outIdx, out := range tx.Vout {
        // Was the output spent?
        if spentTXOs[txID] != nil {
          for _, spentOut := range spentTXOs[txID] {
            if spentOut == outIdx {
              continue Outputs
            }
          }
        }

        if out.CanBeUnlockedWith(address) {
          unspentTXs = append(unspentTXs, *tx)
        }
      }

      if tx.IsCoinbase() == false {
        for _, in := range tx.Vin {
          if in.CanUnlockOutputWith(address) {
            inTxID := hex.EncodeToString(in.Txid)
            spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
          }
        }
      }
    }

    if len(block.PrevBlockHash) == 0 {
      break
    }
  }

  return unspentTXs
}

Поскольку транзакции хранятся в блоках, мы должны проверять каждый блок в цепочке. Начнем с выходов:

if out.CanBeUnlockedWith(address) {
	unspentTXs = append(unspentTXs, tx)
}


Если выход был заблокирован по тому же адресу, мы ищем непотраченные выходы, которые мы хотим. Но перед тем, как принять его, нам нужно проверить, был ли на выходе уже указан вход:

if spentTXOs[txID] != nil {
	for _, spentOut := range spentTXOs[txID] {
		if spentOut == outIdx {
			continue Outputs
		}
	}
}

Мы пропускаем те, на которые уже ссылаются входы (их значения были перенесены на другие выходы, поэтому мы не можем их подсчитать). После проверки выходов мы собираем все входы, которые могут разблокировать выходы, заблокированные с предоставленным адресом (это не относится к coinbase транзакциям, поскольку они не разблокируют выходы):

if tx.IsCoinbase() == false {
    for _, in := range tx.Vin {
        if in.CanUnlockOutputWith(address) {
            inTxID := hex.EncodeToString(in.Txid)
            spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
        }
    }
}

Функция возвращает список транзакций, содержащих непотраченные выходы. Для расчета баланса нам нужна еще одна функция, которая берет транзакции и возвращает только выходы:

func (bc *Blockchain) FindUTXO(address string) []TXOutput {
       var UTXOs []TXOutput
       unspentTransactions := bc.FindUnspentTransactions(address)

       for _, tx := range unspentTransactions {
               for _, out := range tx.Vout {
                       if out.CanBeUnlockedWith(address) {
                               UTXOs = append(UTXOs, out)
                       }
               }
       }

       return UTXOs
}

Готово! Теперь реализуем команду getbalance

func (cli *CLI) getBalance(address string) {
	bc := NewBlockchain(address)
	defer bc.db.Close()

	balance := 0
	UTXOs := bc.FindUTXO(address)

	for _, out := range UTXOs {
		balance += out.Value
	}

	fmt.Printf("Balance of '%s': %d\n", address, balance)
}

Баланс счета — это сумма значений всех непотраченных выходов, заблокированных адресом учетной записи.

Давайте проверим наш баланс после добычи блока генезиса:

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 10

Это наши первые монеты!

Отправка монет


Теперь мы хотим отправить несколько монеты кому-то другому. Для этого нам нужно создать новую транзакцию, поместить ее в блок и обработать его. До сих пор мы реализовали только транзакцию coinbase (которая является специальным типом транзакций), теперь нам нужна общая транзакция:

func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
	var inputs []TXInput
	var outputs []TXOutput

	acc, validOutputs := bc.FindSpendableOutputs(from, amount)

	if acc < amount {
		log.Panic("ERROR: Not enough funds")
	}

	// Build a list of inputs
	for txid, outs := range validOutputs {
		txID, err := hex.DecodeString(txid)

		for _, out := range outs {
			input := TXInput{txID, out, from}
			inputs = append(inputs, input)
		}
	}

	// Build a list of outputs
	outputs = append(outputs, TXOutput{amount, to})
	if acc > amount {
		outputs = append(outputs, TXOutput{acc - amount, from}) // a change
	}

	tx := Transaction{nil, inputs, outputs}
	tx.SetID()

	return &tx
}

Прежде чем создавать новые выходы, нам для начала нужно найти все непотраченные выходы и убедиться, что в них есть достаточное количество монет. Это метод FindSpendableOutputs. После этого для каждого найденного выхода создается вход, ссылающийся на него. Затем мы создаем два выхода:

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

Метод FindSpendableOutputs основан на методе FindUnspentTransactions, который мы определили ранее:

func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) {
	unspentOutputs := make(map[string][]int)
	unspentTXs := bc.FindUnspentTransactions(address)
	accumulated := 0

Work:
	for _, tx := range unspentTXs {
		txID := hex.EncodeToString(tx.ID)

		for outIdx, out := range tx.Vout {
			if out.CanBeUnlockedWith(address) && accumulated < amount {
				accumulated += out.Value
				unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)

				if accumulated >= amount {
					break Work
				}
			}
		}
	}

	return accumulated, unspentOutputs
}

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

Теперь мы можем изменить метод Blockchain.MineBlock:

func (bc *Blockchain) MineBlock(transactions []*Transaction) {
	...
	newBlock := NewBlock(transactions, lastHash)
	...
}

Наконец, создадим команду send:

func (cli *CLI) send(from, to string, amount int) {
	bc := NewBlockchain(from)
	defer bc.db.Close()

	tx := NewUTXOTransaction(from, to, amount, bc)
	bc.MineBlock([]*Transaction{tx})
	fmt.Println("Success!")
}

Отправка монет означает создание транзакции и добавление ее в цепь блоков посредством майнинга блока. Но Биткоин не делает это сразу (как и мы). Вместо этого он помещает все новые транзакции в пул памяти (или mempool), и когда майнер готов к добыче блока, он берет все транзакции из mempool и создает блок-кандидат. Транзакции становятся подтвержденными только тогда, когда блок, содержащий их, добывается и добавляется к цепи блоков.

Давайте проверим, как работает отправка монет:

$ blockchain_go send -from Ivan -to Pedro -amount 6
00000001b56d60f86f72ab2a59fadb197d767b97d4873732be505e0a65cc1e37

Success!

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 4

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 6

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

$ blockchain_go send -from Pedro -to Helen -amount 2
00000099938725eb2c7730844b3cd40209d46bce2c2af9d87c2b7611fe9d5bdf

Success!

$ blockchain_go send -from Ivan -to Helen -amount 2
000000a2edf94334b1d94f98d22d7e4c973261660397dc7340464f7959a7a9aa

Success!

Теперь монеты Хелен заблокированы на двух выходах: один выход от Педро и один от Ивана. Отправим кому-нибудь еще:

$ blockchain_go send -from Helen -to Rachel -amount 3
000000c58136cffa669e767b8f881d16e2ede3974d71df43058baaf8c069f1a0

Success!

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Helen
Balance of 'Helen': 1

$ blockchain_go getbalance -address Rachel
Balance of 'Rachel': 3

Выглядит хорошо! Теперь давайте протестируем исключения:

$ blockchain_go send -from Pedro -to Ivan -amount 5
panic: ERROR: Not enough funds

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2

Заключение


Уф! Это было нелегко, но теперь у нас есть транзакции! Хотя, некоторые ключевые особенности Биткоин-подобной криптовалюты отсутствуют:

  1. Адреса. Пока у нас нет адресов на основе приватного ключа.
  2. Награды. Майнить блоки абсолютно невыгодно!
  3. UTXO. Получение баланса требует сканирования всей цепочки блоков, что может занять очень много времени, когда есть очень много блоков. Кроме того, это займет очень много времени, если мы захотим подтвердить последующие транзакции. UTXO предназначен для решения этих проблем и для быстрой работы с транзакциями.
  4. Mempool. Здесь хранятся транзакции, прежде чем они будут упакованы в блок. В нашей текущей реализации блок содержит только одну транзакцию, и это очень неэффективно.

Ссылки


  1. Full source codes
  2. Transaction
  3. Merkle tree
  4. Coinbase
Tags:
Hubs:
+6
Comments 1
Comments Comments 1

Articles