In this chapter we will learn about the core element of Bitcoin, the language called Script. Script is an embedded programming language that runs inside every Bitcoin node and is responsible for processing transactions. Unlike most other programming languages, it wasn’t designed upfront with formalized grammar and syntax. Instead of using a proper notation technique like Backus-Naur1 to describe its syntax, Script was hard-coded in the very first version of Bitcoin.
Script, the Programming Language
Whenever we talk about sending or receiving funds in Bitcoin’s network, what we really do is execute a series of commands that constitute Bitcoin scripts. Spoken from a very abstract level, we could say that all Bitcoin “sees” are scripts only. There are neither addresses nor bitcoins in Bitcoin’s network. Everything that happens (or fails) in Bitcoin is based on some script execution. To learn “Script”, which is its colloquial name, means to go much deeper into Bitcoin’s code and touching things which are not even familiar to most programmers. Script is very different, both for technical and nontechnical audiences.
A stack is a collection of “things” of any kind that are linearly ordered, where operations can only be done beginning with the top element. This constraint is also called LIFO (last in, first out), because any allowed operations have to begin with the top element. Bitcoin Script uses such a structure to manage and execute commands that come with transactions. As we have already seen in previous chapters, every Bitcoin transaction combines two script parts, a locking and an unlocking script , which then get executed as a single one. If the execution returns a TRUE value, a transaction will be accepted as valid.
Otherwise, the transaction will be rejected by the network. The reason for this seemingly complex behavior lies in the fact that without it no entity participating in the Bitcoin network could ever claim any ownership of bitcoins. There is no option to encode ownership of any funds in Bitcoin’s blockchain as there is no way to define a “bank account” or any other kind of account in it. And because we don’t have them at our disposal, there is no way for us to build relations like “X owns Y bitcoins”. The only thing a party could own is a proof that gives a right to access certain elements, or funds, from the currently available UTXO set. To “own” bitcoins actually means to be in possession of a piece of code that solves the puzzle previously defined by the creator of the UTXO in question. This is mostly done by providing signatures created by using private keys. That’s why wallets aren’t “wallets” at all, but more like keychains as they never hold any bitcoins, but only private keys. Unlike physical ownership of metal coins or paper money, where we have direct access to them, the decentralized ownership relies on signatures as we always have to reference some previous transaction, before spending any bitcoins from them. To have a right to spend funds in real world, we only have to have physical access to them. The same right in decentralized world must be proven by providing a signature that unlocks funds from an unspent transaction that got created in the past.
Simply spoken, when you buy something in the real world, you don’t have to track the past transactions of the coins in your purse. And nobody would ever ask you for such information. You just use coins to buy stuff with them. Not so in Bitcoin. There you must always have a reference to some previous UTXO and a valid proof of the right to change ownership of those funds.
Now imagine Bitcoin’s network with nodes executing a script with similar logic. This would effectively cripple the network by exhausting all of its resources, regardless how powerful the participants are. That’s also the reason why Turing-complete languages like Solidity, one of the scripting languages of Ethereum, which itself runs on top of EVM, the Ethereum Virtual Machine, have the costly “gas consumption” that has to be paid during execution of their programs. Every time a command gets executed, a certain amount of money must be paid or the script execution would stop. This is a strategy to stop malicious actors from exhausting their resources by exploiting the power of Turing-completeness.
Script Notation
We can define Bitcoin scripts as Stack-based programs written in Reverse Polish Notation that are guaranteed to stop at some point in time. Every Bitcoin transaction contains a script which must return a Boolean TRUE to be accepted as valid. To get to this value, one must solve all the puzzles given in the locking script and provide all the data needed via unlocking scripts. And just like any other programming language, Script too has its own set of commands, or as we call them “Op-Codes”, which is short for Operation Codes. Every operation that Script can execute has a certain number assigned to it so that Bitcoin nodes can easily and quickly parse them when reading serialized scripts from the blockchain.
Due to the fact that Bitcoin Script is written in Reverse Polish Notation, the writing and execution of these OP-Codes looks very differently from most of the other programming languages. As we have already seen in the small example before, languages like JavaScript mostly write their statements or object names first (like for or console) and then, if needed, any number of parameters in parentheses. The logic there can be described as command followed by parameters . However, this is not the only way to represent an operation.
We all know the mathematical representation like 2 + 2, where the operator stands between two operands. The Reverse Polish Notation changes the usual mathematical representation by moving the operator behind the operands so that 2 + 2 becomes 2 2 +. At the first sight, the reader would maybe reject this way of writing, but RPN is actually more powerful and better suited for complex operations than the way we learned in school. This has to do with the way it treats operands and operators. When we write 2 2 +, we actually mean put 2 and 2 into the stack and the next operator you read will take as many operands as it needs to complete its operation successfully. In this case the + operator needs two operands, so that it would take both 2s and push back a 4 into the stack. Now imagine a more complex operation like (2 + 3) ∗ (4 - 2) + 10. As we already see, we first need to use parentheses to make sure that (2 + 3) and (4 - 2) will be executed separately from other operations. This isn’t needed in RPN as there we would simply write 2 3 + 4 2 - ∗ 10 +
In Figure 7-4 we start with the two innermost operations and forward their results to operators in higher layers until we have reached the topmost operation that also concludes the script execution.
The first two numbers 2 and 3 would be popped by + and its result, 5, pushed back into the stack. Then the next round of computation will be done, this time 4 and 2, and their result too would be pushed back. Then those two operands (5 and 2) would be multiplied, because ∗ is the next operator that will be executed. Ultimately, their result 10 would be added to the other 10, which would return the final result, 20. As we can see, the RPN is not only ideal for stack-based languages, but it’s also much more suitable for complex operations as it doesn’t need any “supporting” infrastructure like parentheses.
Had this been a Bitcoin Script, the result would be TRUE, because our result is nonzero. In fact, it is allowed to create any kind of script and define any type of unlocking logic as long as we use the allowed OP-Codes and follow certain rules regarding script creation. Of course, a script that doesn’t have any unlocking puzzles included is practically allowing anyone to pilfer the funds. Our following scripts would therefore look a bit more sophisticated than the last example from Figure 7-4.
The complete language of Script comprises of about 100 OP-Codes, but only a handful of them are responsible for the majority of transactions in the current blockchain. Over the next few pages, we will talk about some of them, create a few scripts, and also let them run in controlled environments.
OP-Codes
Cryptographic functions
Stack functions
Flow control
Bitwise operation
Pseudo-words
Arithmetic commands
Regardless of type, every OP-Code is prefixed with OP, like OP_ADD, OP_DUP, etc. There are many older OP-Codes that can’t be used, because they’ve been declared disabled, mostly because of security concerns. For example, several of string-operations have been marked as disabled (OP_CAT, OP_SUBSTR etc.). The adjective “disabled” in this case is actually a misnomer, because there is no valid way to re-enable them ever again, so it’d be better to call them “deleted”.
Most of the allowed Op-Codes can be used in own scripts with the exception of pseudo-words that can never be part of any valid script. Pseudo words are OP_PUBKEY, OP_PUBKEYHASH, and OP_INVALIDOPCODE. These can only be used internally for transaction matching.
The scriptPubKey is being defined by the current owner of funds we want to get access to. The owner defines what’s needed to unlock them, and we provide the solution by delivering our own script, scriptSig, that comprises of a signature and a public key that’s related to the private key we just used to create the signature. This way we can prove that we indeed control the private key whose signature was expected in the locking script. The rest of the operation is already known to us. First, the unlocking script will be appended to scriptPubKey, and then the OP-Codes from scriptPubKey will be executed one by one, each time doing something with the signature and public key we provided, like duplicating the public key, hashing the public key, comparing data, and checking the signature. Here we can see the difference between those two scripts. The previous owner defined the execution logic while we have to provide data that will hopefully keep this logic running and ultimately deliver the needed TRUE value. We have no option to change the logic that comes with scriptPubKey. All we can do is to provide data and wait for the script to complete successfully.
OP_0
OP_1
OP_2 through OP_16
OP_FALSE
OP_TRUE
OP_PUSHDATA1
OP_PUSHDATA2
OP_PUSHDATA4
They serve to push data into the stack and can range from empty arrays like OP_0 to numbers like OP_16. An OP_6, for example, pushes number 6 into the stack, while OP_FALSE pushes a zero, which counts as Boolean FALSE in Bitcoin Script.
Op-Codes for Script Execution Control
Op-Code (Word) | Op-Code (Number) | Purpose |
---|---|---|
OP_NOP | 97 | Do nothing |
OP_IF | 99 | Execute statements if the topmost element in the stack is not False (it also removes the element) |
OP_NOTIF | 100 | Execute statements if the topmost element in the stack is False (it also removes the element) |
OP_ELSE | 103 | Execute statements if preceding IF, ELSE, or NOTIF wasn’t executed; otherwise ignore these statements |
OP_ENDIF | 104 | Mark the end of an IF/ELSE block. All such blocks must end with an ENDIF; otherwise they’ll be rejected as invalid |
OP_VERIFY | 105 | Check if topmost element is True. If not, mark this transaction as invalid; otherwise do nothing. Removes the element |
OP_RETURN | 106 | Mark transaction as invalid. This command always succeeds and is the default way of removing funds from the UTXO set. With this command one can “burn” coins |
Stack Management
Op-Code (Word) | Op-Code (Number) | Purpose |
---|---|---|
OP_DUP | 118 | Duplicate topmost item |
OP_IFDUP | 115 | Duplicate topmost item if not zero |
OP_DROP | 117 | Remove topmost item |
OP_PICK | 121 | Copy n-th item to top of the stack |
OP_ROLL | 122 | Move n-th item to top of the stack |
OP_SWAP | 124 | Swap two topmost items in the stack |
OP_TUCK | 125 | Copy the topmost item before the second-to-top item |
The fourth group of commands are mostly declared as disabled and therefore can’t be used anymore. They have been designed for working with string values, like concatenating or splitting them. The only available command as of now is OP_SIZE that calculates the string-length of the topmost stack and pushes this value to the stack. Other commands from this group are OP_CAT, OP_SUBSTR, OP_LEFT, and OP_RIGHT.
OP_AND
OP_OR
OP_XOR
OP_INVERT
OP_EQUAL
OP_EQUALVERIFY
Only the two last commands are available, and we have already used them in previous scripts. The difference between OP_EQUAL and OP_EQUALVERIFY is that OP_EQUAL only pushes a Boolean result into the stack, while OP_EQUALVERIFY internally runs an OP_VERIFY command that checks the returned value as well. If the result is TRUE, it will continue with script execution; otherwise it’d stop.
OP_MUL
OP_SUB
OP_ADD
OP_DIV
OP_NOT
OP_MOD
and so on.
However, several of them are disabled and therefore can’t be used in any scripts. It is also important to keep in mind that numerical inputs for these Op-Codes are constrained to 32-bit integers, whose return values also can overflow, which puts a great amount of responsibility on the programmer.
Cryptographic Operations
Op-Code (Word) | Op-Code (Number) | Purpose |
---|---|---|
OP_RIPEMD60 | 166 | Get RIPEMD160 hash |
OP_SHA1 | 167 | Get SHA-1 hash |
OP_SHA256 | 168 | Get SHA-256 hash |
OP_HASH160 | 169 | Get a double-hash: first with SHA-256 then with RIPEMD160 |
OP_HASH256 | 170 | Get a double-hash by using two times SHA-256 |
OP_CODESEPARATOR | 171 | Only match signatures to the data after the most recently executed OP_CODESEPARATOR |
OP_CHECKSIG | 172 | Get a hash for all of the transaction inputs, outputs, and script, then compare it with the given signature |
OP_CHECKSIGVERIFY | 173 | Same as OP_CHECKSIG, only with additional OP_VERIFY executed afterward |
OP_CHECKMULTISIG | 174 | Compare given signatures against available public keys. It continues comparing until it has either found enough number of matches (n-of-m) or it stops if there are no more public keys left for comparison |
OP_CHECKMULTISIGVERIFY | 175 | Same as OP_CHECKMUTISIG but with additional OP_VERIFY |
Reserved Words
Op-Code (Word) | Op-Code (Number) | Purpose (Representation) |
---|---|---|
OP_PUBKEYHASH | 253 | Public key hash with OP_HASH160 |
OP_PUBKEY | 254 | Public key compatible with OP_CHECKSIG |
OP_INVALIDOPCODE | 255 | Any unassigned Op-Code |
Reserved words are following Op-Codes: OP_RESERVED, OP_VER, OP_VERIF, OP_VERNOTIF, OP_RESERVED1, OP_RESERVED2, OP_NOP1, and OP_NOP4 through OP_NOP10.
Testing Bitcoin Script with JavaScript
As there is no better way to learn anything than by doing practical examples, we will now build a small JavaScript/NodeJS environment for executing several of the previously mentioned Op-Codes. Being originally designed in C++ and running in an abstract machine, which only deals with raw bytes, learning Bitcoin Script inside Bitcoin’s decentralized network itself is a rather hard task that demands deep knowledge about all of its intricacies, history, and also a good amount of C++ knowledge. To avoid such obstacles without losing practicality, which is always needed in Bitcoin, we will resort to a more welcoming and also easier to use environment based on NodeJS and JavaScript. However, we will still be using raw Script and it’s Op-Codes as they are, so we will still be using Script.
It will add bitcoin-script package to our package.json configuration. You can of course create a completely new package with npm init and then add bitcoin-script, or you can download the prepared environment that comes with this book, then install the packages with npm install, and run it with npm start in the console.
The result would be a Boolen value of TRUE, indicating that the signature corresponds to the given key.
As the whole purpose of Script in Bitcoin is to evaluate if a logic returns a True or False, everything we execute here will ultimately lead to a Boolean value. No matter how complex a script might be, the execution engine will in the end return one of these two possible values. This has to do with the nature of Script programs which are “predicates”. In mathematics the term predicate means a function that can take any data but will always lead to either True or False. The range of input values for these functions is indefinite, while the possible output values are constrained to those two Boolean values.
To get a more visual representation on what’s going on inside the scripting engine and also what its data structure looks like, there is a web site available for visual script execution (Figure 7-5):
https://siminchen.github.io/bitcoinIDE/build/editor.html
Push 2 into the stack.
Push 2 into the stack.
Take two values from the stack (the two “2”), and multiply them.
Push the result of the multiplication into the stack (here, 4).
Push 4 to the stack.
Take two values (the two “4”) from the stack, and compare them for equality.
Push back the result of comparison into the stack. (here, “true”)
Check the topmost element from the stack for “truthiness”.
The final result will be True, and therefore our script execution marked as successful.
Flow Control
What we have done here (Figure 7-6) is a typical if-then-else branching logic, which lets the machine execute certain parts of the program depending on “truthiness” of some value. In our case we wrote the condition “2 < 3” and let the machine check in the if-statement, if it’s true or not. Depending on the outcome, only one of the two possible paths could ever be executed. So far, so easy.
At the first sight, we recognize the usual keywords and different execution branches. The script itself seems to be a simple 1-of-2 multisig, where either of the parties can spend. However, as the IF statement has no visible conditions, the question is, how can any logic from this script be executed? Where does the condition come from?
But how are we supposed to execute IF if there is no condition for it? The answer, again, lies in the way how IF. It always pops data from the stack first and depending on the result (TRUE or FALSE) executes the one or another code branch. This means that in our example, IF would pop one value from the stack and, if it qualifies as true (a non-zero value), execute the first branch. Otherwise, the script would continue with the branch under ELSE.
However, only having provided a condition alone isn’t enough as the two branches contain CHECKSIG that would need two operands to execute properly: signature and public key. The public keys are given, but signatures are missing. Where do they come from? From the unlocking script of course. To execute either branch, the unlocking script must provide two elements: a value and a signature. And depending on the value, one of the two branches will be executed. In practice, this would mean that when Bob wants to spend funds locked by the script, he would have to provide FALSE + BobSignature, while Alice would need to push TRUE + AliceSignature into the stack. The condition in Script functions like a switch for the spending party to activate the “correct” code part. We also call the IF-ELSE-ENDIF constructs “guard clauses”, because they isolate different code parts from each other.
Summary
In this chapter we have learned about the very heart of Bitcoin, it’s scripting language. Anything we do in Bitcoin is basically a script, or part of it. Every single transaction is based on some script that gets validated by every participating node. We have learned about its many commands and what their purposes are. We have also learned how to read Bitcoin Scripts and what constitutes them. We have learned how owners of funds create small locking scripts with puzzles that can only get unlocked by those who provide correct data like signatures and corresponding public keys. We have also learned how to test Op-Codes, the commands of Script, inside a controlled environment based on NodeJS.