Prologue

Participating in the Bitcoin ecosystem inevitably involves on-chain operations. Compared to the transaction building mechanism on Ethereum, constructing a Bitcoin transaction requires some programming skills.

Prerequisite knowledge for this post: UTxO, transaction structure, and scripting language,you can learn these on [Learn me a bitcoin](https://learnmeabitcoin.com/. In addition:

The Bitcoin tool repo: github.com/btcsuite/btcd

Consturct A Simple Transaction

Generate the private key and address

Private key

The private key

Private key have many different forms. The most common form is Wallet Import Format (WIF), and also form in hex. These forms of private key are interchangeable.

Actually, most Chrome wallet extensions not support private key import in WIF format.

You can generate private key and derive private key in WIF format with Golang. It also can generate WIF private key from the online BIP-39 website (remember avoid to use it on mainnet).

1
2
3
4
5
6
7
8
9
10
11
12
// The code in the post all defaults to the test network
cfg := &chaincfg.TestNet3Params

privateKey, err := btcec.NewPrivateKey()
if err != nil {
log.Fatalln(err)
return
}

wif, err := btcutil.NewWIF(privateKey, cfg, true)
fmt.Printf("Generated WIF Key: %s", wif.String())
// Generated WIF Key: cViUtGHsa6XUxxk2Qht23NKJvEzQq5mJYQVFRsEbB1PmSHMmBs4T

Address

The bitcoin address also have many different format, you can learn it in Script on Learn me a bitcoin.

Taproot address is the most frequently used type, which is proposed to support the Taproot protocol and other different Pay-To methods.

The code for generate private key in WIF form is:

1
2
3
4
5
6
7
8
taprootAddr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(
txscript.ComputeTaprootKeyNoScript(
wif.PrivKey.PubKey())),
&chaincfg.TestNet3Params)

log.Printf("Taproot testnet address: %s\n", taprootAddr.String())
// Taproot testnet address: tb1p3d3l9m5d0gu9uykqurm4n8xcdmmw9tkhh8srxa32lvth79kz7vysx9jgcr

There are four nested functions here, from innermost to outermost:

  • wif.PrivKey.PubKey():obtian the public key from WIF private key
  • txscript.ComputeTaprootKeyNoScript:calculate a public key that used to Schnorr signature from the public key
  • schnorr.SerializePubKey:serialize public key to bytecodes
  • btcutil.NewAddressTaproot:generate the address from public key in bytecodes format

Build transaction

The most simplest transaction in Bitcoin is a transaction that just has one input and one output. It transfer the BTC in the input to another address (the receiver).

Generate a random receiver address:tb1pvwak065fek4y0mup9p4l7t03ey2nu8as7zgcrlgm9mdfl8gs5rzss490qd

Although it is said to be a 'simple' transaction, the transaction under Taproot is the most complex transaction in Bitcoin. The simpler transaction type is P2PKH.

Before building a transaction, we need to obtain the available UTxO in wallet. Here is the function GetUnspent(address string) be used to obtain the UTxO. We just fill in manually and returns the required UTxO information.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func GetUnspent(address string) (*wire.OutPoint, *txscript.MultiPrevOutFetcher){
// the transaction hash and the output index
txHash, _ := chainhash.NewHashFromStr(
"7282d54f485561dd21ba22a971b096eb6d0f45ed2fe6bf8c29d87cee162633b4")
point := wire.NewOutPoint(txHash, uint32(0))

// the locking script for the transaction, corresponding to the ScriptPubKey field
script, _ := hex.DecodeString("51208b63f2ee8d7a385e12c0e0f7599cd86ef6e2aed7b9e033762afb177f16c2f309")
output := wire.NewTxOut(int64(1000), script)
fetcher := txscript.NewMultiPrevOutFetcher(nil)
fetcher.AddPrevOut(*point, output)

return point, fetcher
}

It returns a output point and the previous output fetcher.

  • The output point records the transaction hash of the UTxO and the index of the output within the transaction;
  • The fetcher records a mapping that indicates what kind of output corresponds to a given output point

In addition, it is necessary to decode the address and generate a PayToAddress script under Taproot before build transaction, the implementation code as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
func DecodeTaprootAddress(strAddr string, cfg *chaincfg.Params) ([]byte, 
error) {
taprootAddr, err := btcutil.DecodeAddress(strAddr, cfg)
if err != nil {
return nil, err
}

byteAddr, err := txscript.PayToAddrScript(taprootAddr)
if err != nil {
return nil, err
}
return byteAddr, nil
}

We can start constructing a simple transaction now. Unlike Ethereal, where a single JSON object can complete the transaction construction. We need to initialize a empty transaction and fill it in manually.

The input (wire.TxIn) for a new transaction need three parameters: previous output point, signature and witeness script. When constructing the transaction, both of the latter fields are initially set to nil and will be filled in only after the signing is complete.

signature and witeness script

Generally, the witness script and the signature script are independent of each other.

Alternatively, the witness script acts as a type of signature; it exists independently outside the transaction body and can be pruned by nodes after some time.

1
2
3
4
5
6
7
8
9
10
// default version = 1
tx := wire.NewMsgTx(wire.TxVersion)

// use the output point in previous transaction as input
in := wire.NewTxIn(point, nil, nil)
tx.AddTxIn(in)

// create new output, pay to the destination address and fill the amount
out := wire.NewTxOut(int64(800), byteAddr)
tx.AddTxOut(out)

Next, the transaction needs to be signed. The signature is applied to all inputs of the transaction. It is necessary to fill correct unlocking script to the signature or witness field. In Taproot transcation, fill in the witness unlocking script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// obtain the previous transaction
prevOutput := fetcher.FetchPrevOutput(in.PreviousOutPoint)

// sign the transaction by using the private key
witness, _ := txscript.TaprootWitnessSignature(tx,
txscript.NewTxSigHashes(tx, fetcher), 0, prevOutput.Value,
prevOutput.PkScript, txscript.SigHashDefault, wif.PrivKey)

// fill the witness script for input
tx.TxIn[0].Witness = witness

// convert the signed transaction to hex and print
var signedTx bytes.Buffer
tx.Serialize(&signedTx)
finalRawTx := hex.EncodeToString(signedTx.Bytes())

fmt.Printf("Signed Transaction:\n %s", finalRawTx)
// Signed Transaction: 01000000000101b4332616ee7cd8298cbfe62fed450f6deb96b071a922ba21dd6155484fd582720000000000ffffffff01200300000000000022512063bb67ea89cdaa47ef81286bff2df1c9153e1fb0f09181fd1b2eda9f9d10a0c5014011a52fdf6ccdda65359ecc9761b199e132d92bb21be059c6c5fb23e86af7152d429dde23314df0db4bcd52428acffab876b8cca1e19d2788a8382c48141b19bd00000000

This section does not cover the code-level details of sending transactions. In simple terms, broadcasting a transaction means publishing it to any blockchain node.

So, we submit the transaction to Broadcast Transaction

The transaction is https://mempool.space/testnet/tx/f11f3edccb9988729ba4896e1da82b799a7b4e70cca82aa212058076dd49d76f

The complete code for this section:Simple Bitcoin Transaction - Github Gist

Reference

  1. Create Raw Bitcoin Transaction and Sign It With Golang
  2. Learn me a bitcoin