Create your own blockchain using Python (pt. 5)

Transaction scripts

Guillaume Belanger
8 min readJul 14, 2021

Up to now, transaction validation was done using simple cryptography: in order for someone to spend some of his unspent transaction outputs, he had to present proof that the amount was his using a signature. To allow for this type of transactions as well as more complex ones to take place, bitcoin and other cryptocurrencies use transaction scipts. Those scripts define the contract between the sender and the receiver. In this section we will deep-dive into these transaction scripts and learn how they can be implemented and used with Python.

Transaction scripts

In the last section of this tutorial, we implemented a static way to validate transactions by verifying that the signature was valid and that the funds were owned by the sender. Here, we introduce transaction scripts which are a programmatic way to verify transactions. Here is the definition provided by the official bitcoin wiki:

A [transaction] script is a list of instructions recorded with each transaction that describe how the next person wanting to spend the Bitcoins being transferred can gain access to them.

Transaction scripts are made out of two sections, an unlocking script and a locking script and those are part of each transaction. Let’s take a look at a typical bitcoin transaction:

{
'inputs': [
{"transaction_hash": "a26d9501f3fa92ffa9991cf05a72f8b9ca2d66e31e6221cecb66973671a81898", "output_index": 0,
"unlocking_script": "<sender signature> <sender public key>"}],
'outputs': [
{"amount": 10,
"locking_script": "OP_DUP OP_HASH160 <receiver public key hash> OP_EQUAL_VERIFY OP_CHECKSIG"}]
}

What we see is that each transaction input now contains 3 fields: the UTXO hash, the UTXO output index and an unlocking script. The unlocking script is the key to spend the amount from the specified UTXO. For a typical bitcoin transaction, the unlocking script looks like this:

<sig> <pubKey>

Each transaction output contains two field: the amount and the locking script. The locking script encumbers the output with the receiver address. For a typical bitcoin transaction, the locking script looks like this:

OP_DUP OP_HASH160 <pubKeyHash> OP_EQUAL_VERIFY OP_CHECKSIG

The scripting language used inside of bitcoin to conduct those transaction validations is called Script and it was developed specifically for bitcoin. Both the locking and unlocking scripts are written in this language.

To summarize things up, here’s a quote from Andreas M. Antonopoulos’s book (Mastering Bitcoin)

Transaction outputs associate a specific amount to a specific encumbrance or locking script that defines the condition that must be met to spend that amount. In most cases, the locking script will lock the output to a specific bitcoin address, thereby transferring ownership of that amount to the new owner. When Alice paid Bob’s Cafe for a cup of coffee, her transaction created a 0.015 bitcoin output encumbered or locked to the cafe’s bitcoin address. That 0.015 bitcoin output was recorded on the blockchain and became part of the Unspent Transaction Output set, meaning it showed in Bob’s wallet as part of the available balance. When Bob chooses to spend that amount, his transaction will release the encumbrance, unlocking the output by providing an unlocking script containing a signature from Bob’s private key.

Source: Satoshi Nakamoto White Paper: https://bitcoin.org/bitcoin.pdf

Transaction validation

For each transaction, nodes will combine the unlocking script from the transaction’s input to the locking script associated with the UTXO stored on the blockchain and compute the script. If the script passes, the transaction is valid, and if it fails, it’s invalid.

Source: Mastering Bitcoin: Programming the Open Blockchain, Andreas M. Antonopoulos

Script Execution

Here, we will use the most common example which is equivalent to the validations we introduced in the last section. Person 1 wants to send funds to Person 2 and wants only Person 2 to be able to make transactions with those funds in the future. The locking script for this transction would look like this:

OP_DUP OP_HASH160 <pubKeyHash> OP_EQUAL_VERIFY OP_CHECKSIG

Where <pubKeyHash> is Person 2’s public key hash. Now when Person 2 wants to make a transaction to Person 3, he will use the UTXO received from the first transaction and he will provide the unlocking script associated to this transaction. This unlocking script will look like so:

<sig> <pubKey>

When the node receives the transaction, it will look at the inputs’ UTXO and it will retrieve the locking script associated with it from the blockchain. It will then combine those two together (it’s just a simple concatenation):

<sig> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUAL_VERIFY OP_CHECKSIG

Each item that you see is an operation done to a stack. The node will create an empty stack and will execute the operations of this script in the same order as they come. If we read the script out loud, we get:

  1. Push the signature
  2. Push the public key
  3. Complete the OP_DUP operation
  4. Complete the OP_HASH160 operation
  5. Push the public key hash
  6. Complete the OP_EQUAL_VERIFY operation
  7. Complete the OP_CHECKSIG operation

Looking at the bitcoin wiki on Script, we find the following definitions for those operations:

  • OP_DUP: Duplicates the top stack item (<pubKey>)
  • OP_HASH_160: The top stack item (<pubKey>) is hashed twice, first with SHA-256 and then with RIPEMD-160.
  • OP_EQUAL_VERIFY: Fails if the last 2 items in the stack don’t match ( <pubKey> and <pubKeyHash>)
  • OP_CHECK_SIG: The entire transaction’s outputs, inputs, and script are hashed. The signature <sig> is validated against this hash.

Note that there exist more complex types of scripts and we will touch on those later.

Script implementation

Let’s start by updating our transaction input and output data structure to reflect the fact that they now contain scripts. For transaction inputs, we now have:

common/transaction_input.pyimport json


class TransactionInput:
def __init__(self, transaction_hash: str, output_index: int, unlocking_script: str = ""):
self.transaction_hash = transaction_hash
self.output_index = output_index
self.unlocking_script = unlocking_script

def to_json(self, with_unlocking_script: bool = True) -> str:
if with_unlocking_script:
return json.dumps({
"transaction_hash": self.transaction_hash,
"output_index": self.output_index,
"unlocking_script": self.unlocking_script
})
else:
return json.dumps({
"transaction_hash": self.transaction_hash,
"output_index": self.output_index
})

And for transaction outputs:

common/transaction_output.pyimport json


class TransactionOutput:
def __init__(self, public_key_hash: bytes, amount: int):
self.amount = amount
self.locking_script = f"OP_DUP OP_HASH160 {public_key_hash} OP_EQUAL_VERIFY OP_CHECKSIG"

def to_json(self) -> str:
return json.dumps({
"amount": self.amount,
"locking_script": self.locking_script
})

Since we are implementing our blockchain in Python, we won’t be using the Script language, but what we will write will be strongly influenced by it. Now let’s implement such a stack with those operations. We start with a very simple stack:

# node/script.pyclass Stack:
def __init__(self):
self.elements = []

def push(self, element):
self.elements.append(element)

def pop(self):
return self.elements.pop()

We create a new class StackScript that inherits from the Stack class. This new class contains methods that correspond to the operations shown above. It also has an __init__ that will store the current transaction data. This information will be used later by the op_check_sig method.

# node/script.pyclass StackScript(Stack):
def __init__(self, transaction_data: dict):
super().__init__()
for count, tx_input in enumerate(transaction_data["inputs"]):
tx_input_dict = json.loads(tx_input)
tx_input_dict.pop("unlocking_script")
transaction_data["inputs"][count] = json.dumps(tx_input_dict)
self.transaction_data = transaction_data
def op_dup(self):
pass

def op_hash160(self):
pass

def op_equalverify(self):
pass

def op_checksig(self, transaction_data: dict):
pass

Let’s dive into each of those operations. The first is op_dup and simply duplicates the top most element of the stack, which is the public key.

def op_dup(self):
public_key = self.pop()
self.push(public_key)
self.push(public_key)

The next is op_hash_160 that hashes the last element from the stack (the public key) twice.

def op_hash160(self):
public_key = self.pop()
self.push(calculate_hash(calculate_hash(public_key, hash_function="sha256"), hash_function="ripemd160"))

The next is op_equalverify that validates that the last 2 elements from the stack are equal.

def op_equalverify(self):
last_element_1 = self.pop()
last_element_2 = self.pop()
assert last_element_1 == last_element_2

The last one is op_check_sig that validates that the signature from the unlocking script is valid.

def op_checksig(self):
public_key = self.pop()
signature = self.pop()
signature_decoded = binascii.unhexlify(signature.encode("utf-8"))
public_key_bytes = public_key.encode("utf-8")
public_key_object = RSA.import_key(binascii.unhexlify(public_key_bytes))
transaction_bytes = json.dumps(self.transaction_data, indent=2).encode('utf-8')
transaction_hash = SHA256.new(transaction_bytes)
pkcs1_15.new(public_key_object).verify(transaction_hash, signature_decoded)

Script execution

The node looks inside of the unlocking scripts and locking scripts and executing the methods provided blindly. Let’s implement those execution steps in our NodeTransaction

# node/node.pyfrom node.script import StackScriptclass NodeTransaction:

def execute_script(self, unlocking_script, locking_script):
unlocking_script_list = unlocking_script.split(" ")
locking_script_list = locking_script.split(" ")
stack_script = StackScript(self.transaction_data)
for element in unlocking_script_list:
if element.startswith("OP"):
class_method = getattr(StackScript, element.lower())
class_method(stack_script)
else:
stack_script.push(element)
for element in locking_script_list:
if element.startswith("OP"):
class_method = getattr(StackScript, element.lower())
class_method(stack_script)
else:
stack_script.push(element)

Here, we simply create an empty stack, scroll trough each element of each script and apply the methods to the stack.

We also modify our validate method so that the execute_script method is called for every transaction input.

class NodeTransaction:
def validate(self):
for tx_input in self.inputs:
input_dict = json.loads(tx_input)
locking_script = self.get_locking_script_from_utxo(input_dict["transaction_hash"], input_dict["output_index"])
self.execute_script(input_dict["unlocking_script"], locking_script)

For each input, the locking script is retrieved from the UTXO. To do so, we created a new method called get_locking_script_from_utx. This method scrolls trough the blockchain until it finds the UTXO and returns its locking script.

# node/node.pyclass NodeTransaction:

def get_locking_script_from_utxo(self, utxo_hash: str, utxo_index: int):
transaction_data = self.get_transaction_from_utxo(utxo_hash)
return json.loads(transaction_data["outputs"][utxo_index])["locking_script"]

Other transaction scripts

Transaction scripts are at the core of every blockchain transaction. They allow secure validation of transactions and provide a framework to create complex transaction contracts. Validating transactions with the use of scripts allows for a variety of different transaction types to exist. Here are some of them:

  • Pay-to-Public-Key-Hash (P2PKH): This is the most common execution script and it is the one we used as an example above.
Unlocking script: <sig> <pubKey>
Locking script: OP_DUP OP_HASH160 <Cafe Public Key Hash> OP_EQUAL OP_CHECKSIG
  • Pay-to-public-key (P2PK): This method is very similar to P2PKH but takes way more space and requires you to know the whole public key of the person you want to send funds to.
Unlocking script: <sig>
Locking script: <pubKey> OP_CHECKSIG
  • Multi-signature: Multi-signature scripts exist for when you need multiple people to approve a transaction. At least M out of N of those must provide signatures to unlock the funds.
Unlocking script: OP_0 <Signature 1> <Signature 2>
Locking script: M <Public Key 1> <Public Key 2> … <Public Key N> N CHECKMULTISIG
  • Pay-to-script-hash (P2SH): Let’s say you want to receive money from a client and that you want this transaction to be multi-signature (you want to be able to use those funds only if N out of M partners approve it), the locking script the client would have to make would be very long. P2SH scripts exist to simplify complex transactions by hashing those complex scripts .
Unlocking script: Sig1 Sig2 redeem script
Locking script: OP_HASH160 <Hash of redeem script> OP_EQUAL

The blockchain network

While our blockchain is capable of validating transactions (which is already awesome), it is centralized and won’t validate much if your laptop stops running. In the next section, we will introduce the blockchain network which is at the core of cryptocurrencies. The network consists of multiple nodes that communicate with each other via TCP/IP. Each node stores a copy of the blockchain and is responsible of validating transactions and broadcasting them to the other nodes.

Code repository

Create your own blockchain using Python

References

--

--

Guillaume Belanger

Guillaume is a software developer from Montreal who writes about bip bop stuff.