В этом руководстве объясняется, как создать необработанную транзакцию Биткойн с помощью Go, используя библиотеку btcsuite, а затем отправить ее в тестовую сеть или основную сеть.
В учебнике будут рассмотрены следующие концепции:
- UTXO
- Пара открытых и закрытых ключей
- Структура транзакции
- Создать транзакцию
- Подписать транзакцию
- Отправить в сеть
- Комиссия и некоторые другие понятия
UTXO
Неизрасходованная выходная транзакция (UTXO) относится к выходным данным транзакции, которая еще не была потрачена. Баланс и счет - это сумма сумм его UTXO. Например, вы можете выполнить поиск 3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r в blockcypher, а затем нажать Дополнительные сведения, неизрасходованные выходы, чтобы увидеть UTXO этого адреса. Также можно использовать другие адреса или другие исследователи блокчейнов.
Закрытый, открытый ключ и адрес
Мы не углубляемся в закрытые и открытые ключи, а просто касаемся концепции адреса. Чтобы взаимодействовать с блокчейном Биткойн, вы должны предоставить пару закрытых и открытых ключей и извлечь из нее свой адрес. Все биткойн-кошельки выполняют эту работу за вас, и, если вы работаете с основной сетью, сохраните файл резервной копии или начальное число в безопасном месте.
Для тестовой сети можно использовать любой кошелек, поддерживающий тестовую сеть, или можно использовать эту ссылку для получения закрытого ключа и адреса.
Адреса основной сети и тестовой сети немного отличаются из-за алгоритма генерации адреса.
Алгоритм генерации адреса следующий:
(Вы можете пропустить алгоритм генерации адресов, так как каждый кошелек делает это автоматически)
- Получите открытый ключ
- Выполните Sha256 на вашем открытом ключе
- Выполните хеширование RIPEMD160 для результата хеширования Sha256
- Добавьте байт версии перед хешем RIPEMD160 (0x00 для основной сети, 0x6f для тестовой сети)
- Выполнение хеширования Sha256 на расширенном хеш-коде RIPEMD160
- Выполните хеширование Sha256 для результата предыдущего шага
- Возьмите первые 4 байта второго результата Sha256, это контрольная сумма.
- Добавьте 4 байта контрольной суммы на предыдущем шаге в конце хэша RIPEMD160 шага 4 (это 25-байтовый биткойн-адрес)
- Преобразуйте результат из байтовой строки в строку base58
Наш закрытый ключ и адрес для тестовой сети (но не раскрывайте ваш закрытый ключ)
Закрытый ключ: «91izeJtyQ1DNGkiRtMGRKBEKYQTX46Ug8mGtKWpX9mDKqArsLpH»
Адрес: «moLoz9Ao9VTFMKp6AQaAwSVdzhfdfpCGf1»
Структура транзакции
Левая структура описывает транзакцию биткойна, а правая - структура ввода и структура вывода.
- Номер версии: каждой транзакции предшествует 4-байтовый номер версии, который сообщает майнерам, какие правила к ней следует применять.
- Счетчик ввода: показывает, сколько вводов находится в транзакции.
- Входные данные: начинаются с индекса 0
- Счетчик выходных данных: показывает, сколько выходных данных находится в транзакции.
- Выходы: начать с индекса 0
- Время блокировки: указывает самое раннее время, когда транзакция может быть добавлена в цепочку блоков.
Вход:
- Предыдущий Txid: идентификатор транзакции относится к UTXO, который будет потрачен в этой транзакции
- Предыдущий индекс Tx: также известен как «vout» (вектор вывода) и указывает, какой вывод упомянутой транзакции тратится сейчас.
- Длина сценария: длина сценария.
- Сценарий подписи: включая параметры, которые удовлетворяют условиям в скрипте PubKey.
- Последовательность: проверьте последовательность проверьте значение
Выход:
- Ценность: потраченная сумма
- Длина сценария: длина сценария.
- Сценарий PubKey: использование P2PKH (в этом примере) для его создания
Теперь приступим к кодированию
сначала напишите функцию для возврата пустой биткойн-транзакции, используя проводную библиотеку из github.com/btcsuite/btcd/wire. если вы хотите добавить эту библиотеку в свой проект, можете использовать эту команду:
go get github.com/btcsuite/btcd
в каталоге вашего проекта.
package main import ( "github.com/btcsuite/btcd/wire" ) func NewTx() (*wire.MsgTx, error) { return wire.NewMsgTx(wire.TxVersion), nil }
Для добавления входов в нашу транзакцию нам понадобится
- Предыдущий Txid (или хеш предыдущей транзакции)
- PUbKey Script (или script_hex)
- Остаток средств
поэтому мы можем передать эти аргументы нашей функции вручную или получить их от сторонних исследователей блоков, которые предоставляют API для получения UTXO адреса. Blockchair.com дает нам ограниченный доступ к своему API бесплатно, и есть и другие, такие как blockchain.info. Но нужно откуда-то брать UTXO. при использовании стороннего поставщика вы должны демаршалировать ответ JSON и сопоставить ответ со структурой в golang.
Для простоты я работаю только с одним входом и одним выходом, а также необходим жесткий код Txid, Balance и PubKey Script. Но укажите в заключении полный код.
Мы работаем с тестовой сетью Биткойн, если хотите проверить Bit, можете получить отсюда.
func GetUTXO(address string) (string, int64, string, error) { // Provide your url to get UTXOs, read the response // unmarshal it, and extract necessary data var previousTxid string = "16688d2946c3e029ca91ce730109994c2bcafb859d580a6f7c820fb2bb5b6afc" var balance int64 = 62000 var pubKeyScript string = "76a91455d5e92958a8b06b4ff15cd2dd3d254f375e98db88ac" return previousTxid, balance, pubKeyScript, nil }
следующий шаг - добавить ввод и вывод в транзакцию и подписать ее.
func CreateTx(privKey string, destination string, amount int64) (string, error) { wif, err := btcutil.DecodeWIF(privKey) if err != nil { return "", err } // use TestNet3Params for interacting with bitcoin testnet // if we want to interact with main net should use MainNetParams addrPubKey, err := btcutil.NewAddressPubKey(wif.PrivKey.PubKey().SerializeUncompressed(), &chaincfg.TestNet3Params) if err != nil { return "", err } txid, balance, pkScript, err := GetUTXO(addrPubKey.EncodeAddress()) if err != nil { return "", err } /* * 1 or unit-amount in Bitcoin is equal to 1 satoshi and 1 Bitcoin = 100000000 satoshi */ // checking for sufficiency of account if balance < amount { return "", fmt.Errorf("the balance of the account is not sufficient") } // extracting destination address as []byte from function argument (destination string) destinationAddr, err := btcutil.DecodeAddress(destination, &chaincfg.TestNet3Params) if err != nil { return "", err } destinationAddrByte, err := txscript.PayToAddrScript(destinationAddr) if err != nil { return "", err } // creating a new bitcoin transaction, different sections of the tx, including // input list (contain UTXOs) and outputlist (contain destination address and usually our address) // in next steps, sections will be field and pass to sign redeemTx, err := NewTx() if err != nil { return "", err } utxoHash, err := chainhash.NewHashFromStr(txid) if err != nil { return "", err } // the second argument is vout or Tx-index, which is the index // of spending UTXO in the transaction that Txid referred to // in this case is 0, but can vary different numbers outPoint := wire.NewOutPoint(utxoHash, 0) // making the input, and adding it to transaction txIn := wire.NewTxIn(outPoint, nil, nil) redeemTx.AddTxIn(txIn) // adding the destination address and the amount to // the transaction as output redeemTxOut := wire.NewTxOut(amount, destinationAddrByte) redeemTx.AddTxOut(redeemTxOut) // now sign the transaction finalRawTx, err := SignTx(privKey, pkScript, redeemTx) return finalRawTx, nil }
Метод подписи
func SignTx(privKey string, pkScript string, redeemTx *wire.MsgTx) (string, error) { wif, err := btcutil.DecodeWIF(privKey) if err != nil { return "", err } sourcePKScript, err := hex.DecodeString(pkScript) if err != nil { return "", nil } // since there is only one input in our transaction // we use 0 as second argument, if the transaction // has more args, should pass related index signature, err := txscript.SignatureScript(redeemTx, 0, sourcePKScript, txscript.SigHashAll, wif.PrivKey, false) if err != nil { return "", nil } // since there is only one input, and want to add // signature to it use 0 as index redeemTx.TxIn[0].SignatureScript = signature var signedTx bytes.Buffer redeemTx.Serialize(&signedTx) hexSignedTx := hex.EncodeToString(signedTx.Bytes()) return hexSignedTx, nil }
Теперь вы можете отправить подписанную транзакцию в сеть (тестовую сеть) с https://live.blockcypher.com/btc-testnet/pushtx/ или другими способами и проверить, правильно ли вы создали необработанную транзакцию.
Полный код здесь:
package main import ( "bytes" "encoding/hex" "fmt" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" ) func main() { rawTx, err := CreateTx("91izeJtyQ1DNGkiRtMGRKBEKYQTX46Ug8mGtKWpX9mDKqArsLpH", "mkYvnmm3KUBkvVqUAYsG6A6amt5Dva4jzX", 60000) if err != nil { fmt.Println(err) } fmt.Println("raw signed transaction is: ", rawTx) } func NewTx() (*wire.MsgTx, error) { return wire.NewMsgTx(wire.TxVersion), nil } func GetUTXO(address string) (string, int64, string, error) { // Provide your url to get UTXOs, read the response // unmarshal it, and extract necessary data // newURL := fmt.Sprintf("https://your.favorite.block-explorer/%s", address) //response, err := http.Get(newURL) //if err != nil { // fmt.Println("error in GetUTXO, http.Get") // return nil, 0, "", err //} //defer response.Body.Close() //body, err := ioutil.ReadAll(response.Body) // based on the response you get, should define a struct // so before unmarshaling check your JSON response model //var blockChairResp = model.BlockChairResp{} //err = json.Unmarshal(body, &blockChairResp) //if err != nil { // fmt.Println("error in GetUTXO, json.Unmarshal") // return nil, 0, "", err //} var previousTxid string = "16688d2946c3e029ca91ce730109994c2bcafb859d580a6f7c820fb2bb5b6afc" var balance int64= 62000 var pubKeyScript string = "76a91455d5e92958a8b06b4ff15cd2dd3d254f375e98db88ac" return previousTxid, balance, pubKeyScript, nil } func CreateTx(privKey string, destination string, amount int64) (string, error) { wif, err := btcutil.DecodeWIF(privKey) if err != nil { return "", err } // use TestNet3Params for interacting with bitcoin testnet // if we want to interact with main net should use MainNetParams addrPubKey, err := btcutil.NewAddressPubKey(wif.PrivKey.PubKey().SerializeUncompressed(), &chaincfg.TestNet3Params) if err != nil { return "", err } txid, balance, pkScript, err := GetUTXO(addrPubKey.EncodeAddress()) if err != nil { return "", err } /* * 1 or unit-amount in Bitcoin is equal to 1 satoshi and 1 Bitcoin = 100000000 satoshi */ // checking for sufficiency of account if balance < amount { return "", fmt.Errorf("the balance of the account is not sufficient") } // extracting destination address as []byte from function argument (destination string) destinationAddr, err := btcutil.DecodeAddress(destination, &chaincfg.TestNet3Params) if err != nil { return "", err } destinationAddrByte, err := txscript.PayToAddrScript(destinationAddr) if err != nil { return "", err } // creating a new bitcoin transaction, different sections of the tx, including // input list (contain UTXOs) and outputlist (contain destination address and usually our address) // in next steps, sections will be field and pass to sign redeemTx, err := NewTx() if err != nil { return "", err } utxoHash, err := chainhash.NewHashFromStr(txid) if err != nil { return "", err } // the second argument is vout or Tx-index, which is the index // of spending UTXO in the transaction that Txid referred to // in this case is 1, but can vary different numbers outPoint := wire.NewOutPoint(utxoHash, 1) // making the input, and adding it to transaction txIn := wire.NewTxIn(outPoint, nil, nil) redeemTx.AddTxIn(txIn) // adding the destination address and the amount to // the transaction as output redeemTxOut := wire.NewTxOut(amount, destinationAddrByte) redeemTx.AddTxOut(redeemTxOut) // now sign the transaction finalRawTx, err := SignTx(privKey, pkScript, redeemTx) return finalRawTx, nil } func SignTx(privKey string, pkScript string, redeemTx *wire.MsgTx) (string, error) { wif, err := btcutil.DecodeWIF(privKey) if err != nil { return "", err } sourcePKScript, err := hex.DecodeString(pkScript) if err != nil { return "", nil } // since there is only one input in our transaction // we use 0 as second argument, if the transaction // has more args, should pass related index signature, err := txscript.SignatureScript(redeemTx, 0, sourcePKScript, txscript.SigHashAll, wif.PrivKey, false) if err != nil { return "", nil } // since there is only one input, and want to add // signature to it use 0 as index redeemTx.TxIn[0].SignatureScript = signature var signedTx bytes.Buffer redeemTx.Serialize(&signedTx) hexSignedTx := hex.EncodeToString(signedTx.Bytes()) return hexSignedTx, nil }
если запустить
go run main.go
получил этот ответ
raw signed transaction is: 0100000001fc6a5bbbb20f827c6f0a589d85fbca2b4c99090173ce91ca29e0c346298d6816010000008a47304402207772d91e633259fb0cbd35427c4d24806877c5f52e8a8d032505ce3f8b73aa2302201731382284469f8bad48ad2457e1cf445b23c158922f26fb3e3c4fad6298cb5d0141044739edd9fc850cf5db037ecd839ba09f699765d0b13fe8c949688ed3b7ef9291a038729e0c70d6802e3adf1458550922012ebd1e9a979775578eefa867557506ffffffff0160ea0000000000001976a91437383464376cccaf2ae4c8a121805f45bf544e4488ac00000000
и если передать необработанную транзакцию на https://live.blockcypher.com/btc-testnet/pushtx/, вы увидите, что транзакция будет успешно проверена и добавлена в цепочку блоков.
так что теперь вы можете создать необработанную транзакцию, указав правильные данные в функции GetUTXO.
это Txid созданной транзакции в тестовой сети Биткойн: 493c119feb7b546e1057b7c337fffd7192a229d734524f8d920c188049c51705
Платеж
В биткойнах комиссия за транзакцию не указывается явно. Комиссия - это разница между общим количеством входов и общим количеством выходов, поступает на адрес майнера.