Blockchain на Go. Часть 5: Адреса

в 9:15, , рубрики: bitcoin, blockchain, Go, golang, информационная безопасность

Содержание

  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: Сеть

Вступление

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

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

Биткоин-адрес

Вот пример Bitcoin-адреса: 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa. Это самый первый Bitcoin-адрес, который якобы принадлежит Сатоши Накамото. Bitcoin-адреса находятся в открытом доступе. Если вы захотите отправить кому-то монеты, вам нужно знать адрес получателя. Но адреса (несмотря на уникальность) не являются идентификатором вас, как владельца «кошелька». Фактически, такие адреса являются открытым ключом. В Bitcoin ваша личность — это пара (или пары) закрытых и открытых ключей, хранящихся на вашем компьютере (или в каком-либо другом месте, к которому у вас есть доступ). Для создания таких ключей используются криптографические алгоритмы, которые гарантируют, что никто другой не сможет получить доступ к вашим монетам без физического доступа к вашим ключам. Давайте рассмотрим, что же это за алгоритмы.

Криптосистема с открытым ключом

Алгоритмы криптосистем с открытым ключом используют пары ключей: открытые и закрытые. Открытые ключи можно сообщить кому угодно. Закрытые ключи, напротив, не должны раскрываться никому: никто, кроме владельца, не должен иметь к ним доступа, поскольку это личные ключи, которые служат идентификатором владельца. Ваше лицо — это ваши закрытые ключи (в мире криптовалюты, конечно).

По сути, Bitcoin-кошелек — это всего лишь пара таких ключей. Когда вы устанавливаете приложение кошелька или используете Bitcoin клиент для создания нового адреса, для вас генерируется пара ключей. Тот, кто контролирует закрытый ключ, контролирует все монеты, которые были на него отправлены.

Закрытые и открытые ключи — это просто случайные последовательности байтов, поэтому они не могут быть напечатаны на экране и прочитаны человеком. Вот почему Bitcoin использует алгоритм для преобразования открытых ключей в удобочитаемую строку.

Если вы когда-нибудь использовали Bitcoin кошелек в виде приложения, вероятно, для вас была создана мнемоническая фраза. Такие фразы используются вместо закрытых ключей и могут быть использованы для их генерации. Этот механизм реализован в BIP-039.

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

Электронная цифровая подпись

В математике и криптографии существует концепция электронной цифровой подписи — алгоритмы, которые гарантируют:

  1. что данные не были изменены при передаче от отправителя к получателю;
  2. что данные были созданы определенным отправителем;
  3. что отправитель не может отрицать то, что именно он отправил эти данные.

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

Чтобы подписать данные, нам понадобится следующее:

  1. данные для подписи;
  2. закрытый ключ.

Алгоритм создает подпись, которая хранится во входах транзакции. Чтобы проверить подпись, необходимо следующее:

  1. данные, которые были подписаны;
  2. подпись;
  3. открытый ключ.

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

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

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

  1. Проверку того, что входы имеют достаточные права на использование выходов из предыдущих транзакций.
  2. Проверку правильности подписи транзакции.

Схематически, процесс подписи и проверки данных выглядит так:

Blockchain на Go. Часть 5: Адреса - 1

Давайте рассмотрим полный жизненный цикл транзакции:

  1. В самом начале содержится блок генезиса с coinbase транзакцией. В транзакциях coinbase нет реальных входов, поэтому подпись не требуется. Вывод транзакции содержит хешированный открытый ключ (используются алгоритмы RIPEMD16(SHA256(PubKey))).
  2. Когда кто-то отправляет монеты, создается транзакция. Входы транзакции будут ссылаться на выходы из предыдущих транзакций. Каждый вход будет хранить открытый ключ (не хешированный) и подпись всей транзакции.
  3. Другие узлы в сети Bitcoin, которые получат транзакцию, тоже проверят ее. Помимо прочего, здесь происходит сопоставление хеша открытого ключа на входе транзакции с хешом соответствующего выхода, (это гарантирует, что отправитель тратит только принадлежащие ему монеты); подпись правильная (это гарантирует, что транзакция создана реальным владельцем монет).
  4. Когда узел готов к майнингу нового блока, он поместит транзакцию в блок и начнет добывать ее.
  5. Когда блок добыт, каждый другой узел в сети получает сообщение о том, что блок добыли и добавляет его в цепь.
  6. После того, как блок добавлен в цепь, транзакция завершена, теперь на ее выходы можно ссылаться в новых транзакциях.

Эллиптическая криптография

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

Bitcoin использует эллиптические кривые для генерации секретных ключей. Эллиптические кривые — это сложная математическая концепция, которую мы не будем здесь подробно объяснять (если интересно, можете почитать об этом здесь ПРЕДУПРЕЖДЕНИЕ: очень много математики!). Кривая, используемая Bitcoin, может случайным образом выбирать число между 0 и 2²⁵⁶ (что составляет приблизительно 10⁷⁷, на заметку, атомов в видимой вселенной где-то между 10⁷⁸ и 10⁸²). Такой предел означает, что почти невозможно сгенерировать один и тот же закрытый ключ дважды.

Кроме того, Bitcoin использует (и мы будем) алгоритм ECDSA для подписи транзакций.

Base58

Теперь давайте вернемся к вышеупомянутому адресу 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa. Теперь мы знаем, что это общепринятое для человека представление открытого ключа. И если мы его декодируем, он будет выглядеть примерно так (как последовательность байтов, записанных в шестнадцатеричной системе):

0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93

Bitcoin использует алгоритм Base58 для преобразования открытых ключей в читаемый человеком формат. Алгоритм очень похож на известный Base64, но он использует более короткий алфавит: некоторые буквы были удалены из алфавита, чтобы избежать гомографические атаки. В связи с этим, в этом алфавите отсутствуют следующие символы: 0 (ноль), О (заглавная буква «о»), I (заглавная «i»), l (строчная «L») а также знаки "+" и "/".

Примерно так выглядит процесс получения адреса из открытого ключа:

Blockchain на Go. Часть 5: Адреса - 2

Следуя этой схеме, ключ, который мы привели выше, разделен на три части:


Version            00
Public key hash    62E907B15CBF27D5425399EBF6F0FB50EBB88F18
Checksum           C29B7D93

Хорошо, теперь, когда мы собрали все воедино, пришло время писать код! Я надеюсь, что сейчас все, что было непонятным прояснится.

Реализация адресов

Начнем со структуры кошелька Wallet

type Wallet struct {
	PrivateKey ecdsa.PrivateKey
	PublicKey  []byte
}

type Wallets struct {
	Wallets map[string]*Wallet
}

func NewWallet() *Wallet {
	private, public := newKeyPair()
	wallet := Wallet{private, public}

	return &wallet
}

func newKeyPair() (ecdsa.PrivateKey, []byte) {
	curve := elliptic.P256()
	private, err := ecdsa.GenerateKey(curve, rand.Reader)
	pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...)

	return *private, pubKey
}

Кошелек — не что иное, как пара ключей. Нам также понадобится тип Wallets, чтобы хранить коллекцию кошельков, сохранять их в файл и выгружать их из него. В конструкторе Wallet создается новая пара ключей. Функция newKeyPair проста, здесь мы используем ECDSA. Затем создается закрытый ключ с использованием кривой, и открытый ключ генерируется при помощи закрытого ключа. Одно замечание: в алгоритмах на основе эллиптической кривой открытые ключи являются точками на кривой. Таким образом, открытый ключ представляет собой комбинацию координат X, Y. В Bitcoin эти координаты объединяются и образуют открытый ключ.

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

func (w Wallet) GetAddress() []byte {
	pubKeyHash := HashPubKey(w.PublicKey)

	versionedPayload := append([]byte{version}, pubKeyHash...)
	checksum := checksum(versionedPayload)

	fullPayload := append(versionedPayload, checksum...)
	address := Base58Encode(fullPayload)

	return address
}

func HashPubKey(pubKey []byte) []byte {
	publicSHA256 := sha256.Sum256(pubKey)

	RIPEMD160Hasher := ripemd160.New()
	_, err := RIPEMD160Hasher.Write(publicSHA256[:])
	publicRIPEMD160 := RIPEMD160Hasher.Sum(nil)

	return publicRIPEMD160
}

func checksum(payload []byte) []byte {
	firstSHA := sha256.Sum256(payload)
	secondSHA := sha256.Sum256(firstSHA[:])

	return secondSHA[:addressChecksumLen]
}

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

  1. Возьмем открытый ключ и запишем его дважды с помощью алгоритмов хеширования RIPEMD160 (SHA256 (PubKey)).
  2. Подготовим версию.
  3. Вычислим контрольную сумму путем хеширования результата из шага 2 и SHA256 (SHA256 (payload)). Контрольная сумма — это первые четыре байта полученного хеша.
  4. Добавим контрольную сумму в комбинацию version+PubKeyHash.
  5. Зашифруем комбинацию version+PubKeyHash+checksum при помощи Base58.

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

Обратите внимание, что вам не нужно подключаться к узлу Bitcoin для получения адреса. Алгоритм генерации адресов использует комбинацию алгоритмов, которые уже реализованы во многих стандартных библиотеках популярных языков программирования.

Теперь нам нужно изменить входы и выходы для использования адресов:

type TXInput struct {
	Txid      []byte
	Vout      int
	Signature []byte
	PubKey    []byte
}

func (in *TXInput) UsesKey(pubKeyHash []byte) bool {
	lockingHash := HashPubKey(in.PubKey)

	return bytes.Compare(lockingHash, pubKeyHash) == 0
}

type TXOutput struct {
	Value      int
	PubKeyHash []byte
}

func (out *TXOutput) Lock(address []byte) {
	pubKeyHash := Base58Decode(address)
	pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4]
	out.PubKeyHash = pubKeyHash
}

func (out *TXOutput) IsLockedWithKey(pubKeyHash []byte) bool {
	return bytes.Compare(out.PubKeyHash, pubKeyHash) == 0
}

Обратите внимание, что мы больше не используем поля ScriptPubKey и ScriptSig, вместо этого ScriptSig разделяется на поля Signature и PubKey, а ScriptPubKey переименован в PubKeyHash. Мы будем реализовывать те же функции блокировки/разблокировки выходов и логику подписей входов, как в Bitcoin, но реализуем мы это при помощи методов.

Метод UsesKey проверяет, что вход использует определенный ключ для разблокировки выхода. Обратите внимание, что входы хранят нехешированные открытые ключи, а функция принимает хешированный. IsLockedWithKey проверяет, был ли использован хеш-ключ открытого ключа для блокировки выхода. Это дополнительная функция для UsesKey, и они обе используются в FindUnspentTransactions для построения соединений между транзакциями.

Lock просто блокирует выход. Когда мы отправляем монеты кому-то, нам известен только адрес, поэтому функция принимает адрес как единственный аргумент. Затем адрес декодируется, а хеш-ключ открытого ключа извлекается из него и сохраняется в поле PubKeyHash.

Теперь давайте проверим, что все работает правильно:


$ blockchain_go createwallet
Your new address: 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt

$ blockchain_go createwallet
Your new address: 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h

$ blockchain_go createwallet
Your new address: 1Lhqun1E9zZZhodiTqxfPQBcwr1CVDV2sy

$ blockchain_go createblockchain -address 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt
0000005420fbfdafa00c093f56e033903ba43599fa7cd9df40458e373eee724d

Done!

$ blockchain_go getbalance -address 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt
Balance of '13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt': 10

$ blockchain_go send -from 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h -to 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt -amount 5
2017/09/12 13:08:56 ERROR: Not enough funds

$ blockchain_go send -from 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt -to 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h -amount 6
00000019afa909094193f64ca06e9039849709f5948fbac56cae7b1b8f0ff162

Success!

$ blockchain_go getbalance -address 13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt
Balance of '13Uu7B1vDP4ViXqHFsWtbraM3EfQ3UkWXt': 4

$ blockchain_go getbalance -address 15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h
Balance of '15pUhCbtrGh3JUx5iHnXjfpyHyTgawvG5h': 6

$ blockchain_go getbalance -address 1Lhqun1E9zZZhodiTqxfPQBcwr1CVDV2sy
Balance of '1Lhqun1E9zZZhodiTqxfPQBcwr1CVDV2sy': 0

Отлично! Пришла пора реализовать подписи транзакций.

Реализация подписей

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

У нас есть все для реализации подписей к транзакциям, кроме одного: данных для подписи. Какую часть транзакции мы должны подписывать? Или же необходимо подписывать сделку в целом? Выбор данных для подписи очень важен. Дело в том, что данные, которые должны быть подписаны, должны содержать информацию, которая идентифицирует данные уникальным образом. Например, нет смысла подписывать только выходные значения, потому что такая подпись не будет учитывать отправителя и получателя.

Учитывая, что транзакции разблокируют предыдущие выходы, перераспределяют их значения и блокируют новые выходы, должны быть подписаны следующие данные:

  1. Хеши открытых ключей хранящиеся в разблокированных выходах. Это идентифицирует «отправителя» транзакции.
  2. Хеши открытых ключей хранящиеся в новых, заблокированных, выходах. Это идентифицирует «получателя» транзакции.
  3. Значения новых выходов.

В Bitcoin логика блокировки/разблокировки хранится в скриптах, которые хранятся в ScriptSig и ScriptPubKey полей входов и выходов соответственно. Поскольку Bitcoin допускает разные типы таких скриптов, он подписывает все содержимое ScriptPubKey.

В связи с этим в Bitcoin происходит подпись не транзакции, а ее обработанной копии со входами, содержащими ScriptPubKey указанного выхода

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

Все это выглядит достаточно сложным, давайте начнем писать код. А начнем мы с метода Sign:

func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {
	if tx.IsCoinbase() {
		return
	}

	txCopy := tx.TrimmedCopy()

	for inID, vin := range txCopy.Vin {
		prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
		txCopy.Vin[inID].Signature = nil
		txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
		txCopy.ID = txCopy.Hash()
		txCopy.Vin[inID].PubKey = nil

		r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID)
		signature := append(r.Bytes(), s.Bytes()...)

		tx.Vin[inID].Signature = signature
	}
}

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

Давайте внимательнее рассмотрим этот метод:

if tx.IsCoinbase() {
	return
}

Coinbase транзакции не подписаны, так как в них нет реальных выходов

txCopy := tx.TrimmedCopy()

Мы подписываем обработанную копию, а не всю транзакцию:

func (tx *Transaction) TrimmedCopy() Transaction {
	var inputs []TXInput
	var outputs []TXOutput

	for _, vin := range tx.Vin {
		inputs = append(inputs, TXInput{vin.Txid, vin.Vout, nil, nil})
	}

	for _, vout := range tx.Vout {
		outputs = append(outputs, TXOutput{vout.Value, vout.PubKeyHash})
	}

	txCopy := Transaction{tx.ID, inputs, outputs}

	return txCopy
}

Копия будет включать все входы и выходы, а TXInput.Signature и TXInput.PubKey будут равны nil.

Затем пройдемся по каждому входу в копии:

for inID, vin := range txCopy.Vin {
	prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
	txCopy.Vin[inID].Signature = nil
	txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash

На каждом входе Signature устанавливается на nil (просто двойная проверка), а PubKey присваиваем ссылку на выход в PubKeyHash. В настоящий момент все транзакции, кроме текущей, являются «пустыми», то есть поля подписи и PubKey равны нулю. Таким образом, входы подписываются отдельно, хотя это необязательно для нашего приложения, но Bitcoin позволяет транзакциям содержать входы, ссылающиеся на различные адреса.

        txCopy.ID = txCopy.Hash()
	txCopy.Vin[inID].PubKey = nil

Метод Hash сериализует транзакцию и хеширует ее с помощью алгоритма SHA-256. Результатом являются данные готовые для подписи. После получения хеша мы должны сбросить поле PubKey, чтобы не было влияния на наши дальнейшие итерации.

        r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID)
	signature := append(r.Bytes(), s.Bytes()...)

	tx.Vin[inID].Signature = signature

Мы подписываем txCopy.ID при помощи privKey. Подпись ECDSA представляет собой пару чисел, которые мы объединяем и сохраняем в поле входа Signature.

Рассмотрим функцию верификации:

func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
	txCopy := tx.TrimmedCopy()
	curve := elliptic.P256()

	for inID, vin := range tx.Vin {
		prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
		txCopy.Vin[inID].Signature = nil
		txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
		txCopy.ID = txCopy.Hash()
		txCopy.Vin[inID].PubKey = nil

		r := big.Int{}
		s := big.Int{}
		sigLen := len(vin.Signature)
		r.SetBytes(vin.Signature[:(sigLen / 2)])
		s.SetBytes(vin.Signature[(sigLen / 2):])

		x := big.Int{}
		y := big.Int{}
		keyLen := len(vin.PubKey)
		x.SetBytes(vin.PubKey[:(keyLen / 2)])
		y.SetBytes(vin.PubKey[(keyLen / 2):])

		rawPubKey := ecdsa.PublicKey{curve, &x, &y}
		if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false {
			return false
		}
	}

	return true
}

Метод довольно простой. Для начала получим копию транзакции, как в прошлом методе:

txCopy := tx.TrimmedCopy()

Затем нам понадобится кривая, которая используется для генерации пар ключей:

curve := elliptic.P256()

Затем пройдемся по всем входам и проверим, что они подписаны:

for inID, vin := range tx.Vin {
	prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
	txCopy.Vin[inID].Signature = nil
	txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
	txCopy.ID = txCopy.Hash()
	txCopy.Vin[inID].PubKey = nil

Эта часть идентична той, что используется в методе Sign, так как во время проверки нам нужны те же самые данные, что мы и подписывали

	r := big.Int{}
	s := big.Int{}
	sigLen := len(vin.Signature)
	r.SetBytes(vin.Signature[:(sigLen / 2)])
	s.SetBytes(vin.Signature[(sigLen / 2):])

	x := big.Int{}
	y := big.Int{}
	keyLen := len(vin.PubKey)
	x.SetBytes(vin.PubKey[:(keyLen / 2)])
	y.SetBytes(vin.PubKey[(keyLen / 2):])

Здесь мы распаковываем значения, хранящиеся в TXInput.Signature и TXInput.PubKey, так как сигнатура представляет собой пару чисел, а открытый ключ — это пара координат. Мы конкатенировали их раньше для хранения, и теперь нам нужно их распаковать для использования в функциях crypto/ecdsa.

	rawPubKey := ecdsa.PublicKey{curve, &x, &y}
	if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false {
		return false
	}
}

return true

Теперь мы создаем ecdsa.PublicKey, используя открытый ключ, который мы берем из входа, и выполняем ecdsa.Verify, передавая подпись, из входа. Если все входы проверены, мы возвращаем true; если хотя бы один вход не прошел проверку, возвращаем false.

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

func (bc *Blockchain) FindTransaction(ID []byte) (Transaction, error) {
	bci := bc.Iterator()

	for {
		block := bci.Next()

		for _, tx := range block.Transactions {
			if bytes.Compare(tx.ID, ID) == 0 {
				return *tx, nil
			}
		}

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

	return Transaction{}, errors.New("Transaction is not found")
}

func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) {
	prevTXs := make(map[string]Transaction)

	for _, vin := range tx.Vin {
		prevTX, err := bc.FindTransaction(vin.Txid)
		prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
	}

	tx.Sign(privKey, prevTXs)
}

func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool {
	prevTXs := make(map[string]Transaction)

	for _, vin := range tx.Vin {
		prevTX, err := bc.FindTransaction(vin.Txid)
		prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
	}

	return tx.Verify(prevTXs)
}

FindTransaction находит транзакцию по идентификатору (для этого требуется итерация по всем блокам в цепи); SignTransaction берет одну транзакцию, находит транзакции, на которые она ссылается, и подписывает ее; VerifyTransaction просто проверяет транзакцию.

Теперь нам нужно подписать и проверить транзакции. Подписывать мы будем в методе NewUTXOTransaction:

func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
	...

	tx := Transaction{nil, inputs, outputs}
	tx.ID = tx.Hash()
	bc.SignTransaction(&tx, wallet.PrivateKey)

	return &tx
}

Проверка транзакции происходит до того, как она будет добавлена в блок:

func (bc *Blockchain) MineBlock(transactions []*Transaction) {
	var lastHash []byte

	for _, tx := range transactions {
		if bc.VerifyTransaction(tx) != true {
			log.Panic("ERROR: Invalid transaction")
		}
	}
	...
}

Вот и все! Давайте проверим все еще раз:


$ blockchain_go createwallet
Your new address: 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR

$ blockchain_go createwallet
Your new address: 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab

$ blockchain_go createblockchain -address 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR
000000122348da06c19e5c513710340f4c307d884385da948a205655c6a9d008

Done!

$ blockchain_go send -from 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR -to 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab -amount 6
0000000f3dbb0ab6d56c4e4b9f7479afe8d5a5dad4d2a8823345a1a16cf3347b

Success!

$ blockchain_go getbalance -address 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR
Balance of '1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR': 4

$ blockchain_go getbalance -address 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab
Balance of '1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab': 6

У нас даже ничего не сломалось, удивительно!

Давайте закомментируем вызов bc.SignTransaction (& tx, wallet.PrivateKey) в NewUTXOTransaction, для гарантии того, что неподписанные транзакции нельзя будет майнить:

func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
   ...
	tx := Transaction{nil, inputs, outputs}
	tx.ID = tx.Hash()
	// bc.SignTransaction(&tx, wallet.PrivateKey)

	return &tx
}


$ go install
$ blockchain_go send -from 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR -to 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab -amount 1
2017/09/12 16:28:15 ERROR: Invalid transaction

Заключение

Мы реализовали почти все ключевые особенности Bitcoin и это потрясающе. А в следующей части мы наконец-таки завершим реализацию транзакций.

Ссылки

  1. Full source codes
  2. Public-key cryptography
  3. Digital signatures
  4. Elliptic curve
  5. Elliptic curve cryptography
  6. ECDSA
  7. Technical background of Bitcoin addresses
  8. Address
  9. Base58
  10. A gentle introduction to elliptic curve cryptography

Автор: Iambeardier

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js