About a month ago I had written about creating and importing private keys as well as generating public addresses for Bitcoin and several other cryptocurrencies using the Go programming language. This previous tutorial had more of an emphasis on creating cryptocurrency wallets with Golang than anything.
The next step in making Bitcoin and other cryptocurrencies useful is to be able to transfer or send them to other people. Sending Bitcoin is part of a process known as creating and broadcasting a transaction.
While we won’t be actually broadcasting a transaction in this tutorial, we’re going to figure out how to create an unsigned transaction, then sign it, using the Go programming language and some popular Bitcoin packages.
Going forward, it must be made clear that I am not a cryptocurrency or cryptography expert. If you’ve found bugs in what I’ve shared, please let me know in the comments. If you’re just planning to copy and paste the code I’ve provided, please do so understanding that it is at your own risk.
Before we get into the coding, we need to understand how transactions work. It is an unnecessarily difficult concept that too me quite a while to figure out using Golang. It is much easier with other technologies like Node.js because the libraries take the guess work out of it.
When working towards creating a transaction, we have the following parts:
Using the above, further data must be derived and be a part of the transaction, but before we get there, let’s figure out what the above parts will represent to us. If you want an explanation of concepts, far deeper than mine, check out Bitcoins the Hard Way: Using the Raw Bitcoin Protocol, which is where I got the fundamentals necessary to accomplish the task in Go.
The private key is used for two things. When we have an unsigned transaction, the private key will be used to sign the transaction proving that the funds can be spent. The sender’s public key and public address can also be derived from the private key. We’ll get to that in a moment.
When someone is sent Bitcoin, a transaction is made, which is what we’re trying to do. On the receiving end, when everything is said and done, we’re left with what is called unspent transaction output (UTXO). When we wish to turn around and send, we’re creating a transaction from the UTXO. The UTXO has information such as the transaction hash id, the public key script, and the amount of satoshi in the transaction. We can figure out the public key script through a public address. We’ll be creating an unspent transaction output, hence we’ll need to come up with a transaction id and make a public key script with the sender’s information.
The recipient’s public address is where the funds will be sent. A public key script will be created and added to a redeeming transaction rather than the source UTXO transaction.
Finally, the amount in satoshis must be determined. This amount must exist in the UTXO.
While we just saw a brief overview, I’m hoping it should be enough to get us situated for the Go code. Like I said previously, check out the Bitcoins the Hard Way: Using the Raw Bitcoin Protocol tutorial if you want something crazy in depth.
There will be quite a bit happening in our Go application to make transactions possible. For simplicity, we’re only going to be using a single input and a single output. We’ll explore multiple inputs and multiple outputs in a future tutorial. We’re also going to be fabricating our own UTXO rather than using a real UTXO, meaning a broadcast will fail. We can’t spend Bitcoin that we don’t own.
Create a project within your $GOPATH. I’ll be adding all my code to a main.go file, but feel free to design your project however you want. Before we add code, we need to get our package dependencies.
From the command line, execute the following:
go get github.com/btcsuite/btcd
go get github.com/btcsuite/btcutil
Both btcd and btcutil are part of a very popular Bitcoin package for Go.
Open your project’s main.go file and include the following:
package main
import (
"bytes"
"encoding/hex"
"encoding/json"
"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"
)
type Transaction struct {
TxId string `json:"txid"`
SourceAddress string `json:"source_address"`
DestinationAddress string `json:"destination_address"`
Amount int64 `json:"amount"`
UnsignedTx string `json:"unsignedtx"`
SignedTx string `json:"signedtx"`
}
func CreateTransaction(secret string, destination string, amount int64, txHash string) (Transaction, error) { }
func main() { }
When we create our transaction, we’ll store the data in a Transaction
object. If you’re planning to store transaction data locally, this object has the information that you’d probably want, none of which is sensitive. However, the signed transaction data can be broadcasted just once.
In the CreateTransaction
function a few things are going to happen.
So let’s figure out what each of the above steps looks like in Go.
To import our WIF string, we can use the DecodeWIF
function:
wif, err := btcutil.DecodeWIF(secret)
if err != nil {
return Transaction{}, err
}
With the private key loaded and hopefully correctly formatted, we can start obtaining the public key script and creating our origin transaction which includes previous transaction hash and spend outputs that will eventually be inputs for the redeeming transaction.
addresspubkey, _ := btcutil.NewAddressPubKey(wif.PrivKey.PubKey().SerializeUncompressed(), &chaincfg.MainNetParams)
sourceTx := wire.NewMsgTx(wire.TxVersion)
sourceUtxoHash, _ := chainhash.NewHashFromStr(txHash)
sourceUtxo := wire.NewOutPoint(sourceUtxoHash, 0)
sourceTxIn := wire.NewTxIn(sourceUtxo, nil, nil)
destinationAddress, err := btcutil.DecodeAddress(destination, &chaincfg.MainNetParams)
sourceAddress, err := btcutil.DecodeAddress(addresspubkey.EncodeAddress(), &chaincfg.MainNetParams)
if err != nil {
return Transaction{}, err
}
destinationPkScript, _ := txscript.PayToAddrScript(destinationAddress)
sourcePkScript, _ := txscript.PayToAddrScript(sourceAddress)
sourceTxOut := wire.NewTxOut(amount, sourcePkScript)
sourceTx.AddTxIn(sourceTxIn)
sourceTx.AddTxOut(sourceTxOut)
sourceTxHash := sourceTx.TxHash()
The previous transaction hash can be anything for this example since we’re not broadcasting. I believe this is referred to as a raw transaction that we’re building since it can be done offline.
The sourceTxHash
variable is our new transaction id for the redeem transaction. When the recipient wants to send Bitcoin, they will use this new transaction id like we’re using the old transaction id.
With the origin transaction we can create the redeem transaction:
redeemTx := wire.NewMsgTx(wire.TxVersion)
prevOut := wire.NewOutPoint(&sourceTxHash, 0)
redeemTxIn := wire.NewTxIn(prevOut, nil, nil)
redeemTx.AddTxIn(redeemTxIn)
redeemTxOut := wire.NewTxOut(amount, destinationPkScript)
redeemTx.AddTxOut(redeemTxOut)
Notice that the input is now our new transaction, or the previous UTXO. The output is the amount that we’re sending to the recipient.
Since we have a redeem transaction, we need to sign it with the senders private key that was loaded:
sigScript, err := txscript.SignatureScript(redeemTx, 0, sourceTx.TxOut[0].PkScript, txscript.SigHashAll, wif.PrivKey, false)
if err != nil {
return Transaction{}, err
}
redeemTx.TxIn[0].SignatureScript = sigScript
The sigScript
variable contains the signature as well as the public key of the sender. An invalid signature script will result in a poorly signed transaction that the recipient won’t be able to read. To prevent trying to broadcast a bad transaction, we can actually validate it:
flags := txscript.StandardVerifyFlags
vm, err := txscript.NewEngine(sourceTx.TxOut[0].PkScript, redeemTx, 0, flags, nil, nil, amount)
if err != nil {
return Transaction{}, err
}
if err := vm.Execute(); err != nil {
return Transaction{}, err
}
If no errors pop up, we should be good to go, at which point we can add our data to the Transaction
object like so:
var unsignedTx bytes.Buffer
var signedTx bytes.Buffer
sourceTx.Serialize(&unsignedTx)
redeemTx.Serialize(&signedTx)
transaction.TxId = sourceTxHash.String()
transaction.UnsignedTx = hex.EncodeToString(unsignedTx.Bytes())
transaction.Amount = amount
transaction.SignedTx = hex.EncodeToString(signedTx.Bytes())
transaction.SourceAddress = sourceAddress.EncodeAddress()
transaction.DestinationAddress = destinationAddress.EncodeAddress()
return transaction, nil
To actually try this function, let’s add some fake, but correctly formatted data via the main
function:
func main() {
transaction, err := CreateTransaction("5HusYj2b2x4nroApgfvaSfKYZhRbKFH41bVyPooymbC6KfgSXdD", "1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa", 91234, "81b4c832d70cb56ff957589752eb4125a4cab78a25a8fc52d6a09e5bd4404d48")
if err != nil {
fmt.Println(err)
return
}
data, _ := json.Marshal(transaction)
fmt.Println(string(data))
}
The WIF string and public address information above is real, taken from that in-depth tutorial I mentioned. The transaction id is also real, but can be anything as we’re not going to broadcast.
When running the above, you should end up with something like the following:
{
"txid": "4e8378675bcf6a389c8cfe246094aafa44249e48ab88a40e6fda3bf0f44f916a",
"source_address": "1MMMMSUb1piy2ufrSguNUdFmAcvqrQF8M5",
"destination_address": "1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa",
"amount": 91234,
"unsignedtx": "0100000001484d40d45b9ea0d652fca8258ab7caa42541eb52975857f96fb50cd732c8b4810000000000ffffffff0162640100000000001976a914df3bd30160e6c6145baaf2c88a8844c13a00d1d588ac00000000",
"signedtx": "01000000016a914ff4f03bda6f0ea488ab489e2444faaa946024fe8c9c386acf5b6778834e000000008b483045022100904dbeddeecccf6391ac92922381ae006bf244c002f42e195daa0a9837a4b5820220461677f9dbb7d9580e268ac486cfeb4b9d87bfdd6d4e7b1be09b8e6f5cc0a70701410414e301b2328f17442c0b8310d787bf3d8a404cfbd0704f135b6ad4b2d3ee751310f981926e53a6e8c39bd7d3fefd576c543cce493cbac06388f2651d1aacbfcdffffffff0162640100000000001976a914c8e90996c7c6080ee06284600c684ed904d14c5c88ac00000000"
}
To validate that it is not nonsense data, you can use an online validation tool like TXID.IO. I used it aggressively when testing because I couldn’t grasp how transactions should be formatted. I don’t recommend using real keys in case your computer becomes compromised. Use testnet values when testing.
In case the bits and pieces that I threw together isn’t enough and you needed the full picture, I’m providing the source code to the above application, even though it is the same thing:
package main
import (
"bytes"
"encoding/hex"
"encoding/json"
"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"
)
type Transaction struct {
TxId string `json:"txid"`
SourceAddress string `json:"source_address"`
DestinationAddress string `json:"destination_address"`
Amount int64 `json:"amount"`
UnsignedTx string `json:"unsignedtx"`
SignedTx string `json:"signedtx"`
}
func CreateTransaction(secret string, destination string, amount int64, txHash string) (Transaction, error) {
var transaction Transaction
wif, err := btcutil.DecodeWIF(secret)
if err != nil {
return Transaction{}, err
}
addresspubkey, _ := btcutil.NewAddressPubKey(wif.PrivKey.PubKey().SerializeUncompressed(), &chaincfg.MainNetParams)
sourceTx := wire.NewMsgTx(wire.TxVersion)
sourceUtxoHash, _ := chainhash.NewHashFromStr(txHash)
sourceUtxo := wire.NewOutPoint(sourceUtxoHash, 0)
sourceTxIn := wire.NewTxIn(sourceUtxo, nil, nil)
destinationAddress, err := btcutil.DecodeAddress(destination, &chaincfg.MainNetParams)
sourceAddress, err := btcutil.DecodeAddress(addresspubkey.EncodeAddress(), &chaincfg.MainNetParams)
if err != nil {
return Transaction{}, err
}
destinationPkScript, _ := txscript.PayToAddrScript(destinationAddress)
sourcePkScript, _ := txscript.PayToAddrScript(sourceAddress)
sourceTxOut := wire.NewTxOut(amount, sourcePkScript)
sourceTx.AddTxIn(sourceTxIn)
sourceTx.AddTxOut(sourceTxOut)
sourceTxHash := sourceTx.TxHash()
redeemTx := wire.NewMsgTx(wire.TxVersion)
prevOut := wire.NewOutPoint(&sourceTxHash, 0)
redeemTxIn := wire.NewTxIn(prevOut, nil, nil)
redeemTx.AddTxIn(redeemTxIn)
redeemTxOut := wire.NewTxOut(amount, destinationPkScript)
redeemTx.AddTxOut(redeemTxOut)
sigScript, err := txscript.SignatureScript(redeemTx, 0, sourceTx.TxOut[0].PkScript, txscript.SigHashAll, wif.PrivKey, false)
if err != nil {
return Transaction{}, err
}
redeemTx.TxIn[0].SignatureScript = sigScript
flags := txscript.StandardVerifyFlags
vm, err := txscript.NewEngine(sourceTx.TxOut[0].PkScript, redeemTx, 0, flags, nil, nil, amount)
if err != nil {
return Transaction{}, err
}
if err := vm.Execute(); err != nil {
return Transaction{}, err
}
var unsignedTx bytes.Buffer
var signedTx bytes.Buffer
sourceTx.Serialize(&unsignedTx)
redeemTx.Serialize(&signedTx)
transaction.TxId = sourceTxHash.String()
transaction.UnsignedTx = hex.EncodeToString(unsignedTx.Bytes())
transaction.Amount = amount
transaction.SignedTx = hex.EncodeToString(signedTx.Bytes())
transaction.SourceAddress = sourceAddress.EncodeAddress()
transaction.DestinationAddress = destinationAddress.EncodeAddress()
return transaction, nil
}
func main() {
transaction, err := CreateTransaction("5HusYj2b2x4nroApgfvaSfKYZhRbKFH41bVyPooymbC6KfgSXdD", "1KKKK6N21XKo48zWKuQKXdvSsCf95ibHFa", 91234, "81b4c832d70cb56ff957589752eb4125a4cab78a25a8fc52d6a09e5bd4404d48")
if err != nil {
fmt.Println(err)
return
}
data, _ := json.Marshal(transaction)
fmt.Println(string(data))
}
Remember to do your own research on transactions and test before trying to create transactions with production keys.
You just saw how to create and sign Bitcoin transactions, offline, with the Go programming language. Like I said earlier, I am not a cryptocurrency expert, nor am I a cryptography expert. If you’ve found a bug, please explain it in the comments of this article.
Many of the concepts of this tutorial were taken from the Bitcoins the Hard Way: Using the Raw Bitcoin Protocol article which is incredibly helpful. The btcd and btcutil packages have minimal documentation dispersed across everywhere. A lot of the code in this tutorial is Frankenstein code from various locations in the GoDocs.
If you want to learn how to apply this tutorial towards creating a fully functional cryptocurrency hardware wallet, check out the tutorial I wrote titled, Create a Bitcoin Hardware Wallet with Golang and a Raspberry Pi Zero.