Create Raw Bitcoin Transaction and Sign It With Golang

Mahdi Darabi
Nov 13, 2020 · 8 min read

This tutorial explains how to create a raw Bitcoin transaction with Go, using btcsuite library, and then push it to the testnet, or main net.

Following concepts will be covered by the tutorial:

  • UTXO
  • Public, Private key pair
  • Transaction Structure
  • Create Transaction
  • Sign Transaction
  • Push to network
  • Fee and some other concepts

UTXO

Unspent Output Transaction (UTXO) is referred to an output of a transaction that is not spent yet. The balance of and account is the sum of the amounts of its UTXOs. For example, you can search “3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r” in blockcypher and then click on “Advanced Details”, “unpent outputs” to see UTXOs of this address. Can also use other addresses or other blockchain explorers.

UTXO (blockchain explorer)

Private, Public key and Address

We don’t dive deep in private and public key, and just touch address concept. To interact with Bitcoin blockchain, you have to provide a private, public key pair and extract your address from it. All Bitcoin wallets do this job for you and if working with the main net store your backup file or the seed in a secure place.

For testnet, any wallet that supports testnet can be used, or can use this link to get private key and address.

Main net and testnet addresses have a little difference, because of the generating address algorithm.

Address generating algorithm is as follows:

(You can skip address generating algorithm, since every wallet does it for automatically)

  • Get you public key
  • Perform Sha256 on your Public key
  • Perform RIPEMD160 hashing on the result of Sha256 hash
  • Add version byte in front of RIPEMD160 hash (0x00 for main net, 0x6f for testnet)
  • Perform Sha256 hash on the extended RIPEMD160 hash
  • Perform Sha256 hash on the result of the previous step
  • Take the first 4 byte of the second Sha256 result, it’s the checksum
  • Add 4 bytes of checksum in previous step, at the end of RIPEMD160 hash of step 4 (This is 25-byte bitcoin address)
  • Convert the result from byte string to base58 string

Our private key and address for testnet (but don’t reveal your private key)

Private key: “91izeJtyQ1DNGkiRtMGRKBEKYQTX46Ug8mGtKWpX9mDKqArsLpH”

Address: “moLoz9Ao9VTFMKp6AQaAwSVdzhfdfpCGf1”

Transaction Structure

Bitcoin transaction structure

The left structure describe bitcoin transaction and in right of it, there are input struct and output struct.

  • Version Number: each transaction is prefixed by a 4-byte version number that tells the miners which rules should be applied to it
  • Input Counter: tells how many inputs are in the transaction
  • Inputs: start with index 0
  • Output Counter: tells how many output are in the transaction
  • Outputs: start with index 0
  • Locktime: indicates the earliest time a transaction can be added to the blockchain

Input:

  • Previous Txid: the transaction identifier refer to the UTXO that will be spent in this transaction
  • Previous Tx index: also known as “vout” (output vector) and imply which output of referred transaction is spent now
  • Script Length: the length of the script
  • Signature script: including the parameters which satisfy the conditions in PubKey Script
  • Sequence: check sequence verify the value

Output:

  • Value: the amount being spent
  • Script Length: the length of the script
  • PubKey Script: using P2PKH (in this example) to constructing it

Now start coding

first write a function to give back an empty bitcoin transaction, using “wire” library from github.com/btcsuite/btcd/wire . if you want to add this library to your project can use this command:

go get github.com/btcsuite/btcd

in your project directory.

package main

import (
"github.com/btcsuite/btcd/wire"
)

func NewTx() (*wire.MsgTx, error) {
return wire.NewMsgTx(wire.TxVersion), nil
}

For adding inputs into our transaction we need

  • Previous Txid (or previous transaction hash)
  • PUbKey Script (or script_hex)
  • Balance

so we can pass these arguments manually to our function or get them form a third-party like block-explorers that provide API to get UTXOs of an address. blockchair.com gives us a limited access to its API for free and there are others like blockchain.info. But it’s necessary to get UTXOs from somewhere. if using a third-party you should unmarshal the JSON response, and map the response to a struct in golang.

For simplicity, I work with only one input and one output, and also hard-code needed Txid, Balance, and PubKey Script. But provide the complete code in final.

We work with Bitcoin testnet, if want test Bit, can get from here.

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
}

next step is to add input and output in the transaction, and sign it

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
}

Signing method

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
}

Now you can push the signed transaction to network (testnet) from https://live.blockcypher.com/btc-testnet/pushtx/ or other ways, and check if you create your raw transaction properly or not.

Full code is here:

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
}

if run

go run main.go

got this response

raw signed transaction is:  0100000001fc6a5bbbb20f827c6f0a589d85fbca2b4c99090173ce91ca29e0c346298d6816010000008a47304402207772d91e633259fb0cbd35427c4d24806877c5f52e8a8d032505ce3f8b73aa2302201731382284469f8bad48ad2457e1cf445b23c158922f26fb3e3c4fad6298cb5d0141044739edd9fc850cf5db037ecd839ba09f699765d0b13fe8c949688ed3b7ef9291a038729e0c70d6802e3adf1458550922012ebd1e9a979775578eefa867557506ffffffff0160ea0000000000001976a91437383464376cccaf2ae4c8a121805f45bf544e4488ac00000000

and if pass the raw transaction to https://live.blockcypher.com/btc-testnet/pushtx/ , can see the transaction will successfully verified and add to the blockchain.

so now you can build raw transaction, by providing correct data in GetUTXO function.

this is the Txid of the created transaction on Bitcoin testnet: 493c119feb7b546e1057b7c337fffd7192a229d734524f8d920c188049c51705

Fee

In a Bitcoin transaction fee is not declared explicitly. Fee is the difference between total amount of inputs and total amount of outputs, goes to miner address.

The Startup

Get smarter at building your thing. Join The Startup’s +787K followers.

Sign up for Top 10 Stories

By The Startup

Get smarter at building your thing. Subscribe to receive The Startup's top 10 most read stories — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +787K followers.

Mahdi Darabi

Written by

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +787K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store