Building a Turing machine on Bitcoin Cash (NSFW)

Disclaimer: This post contains some profanities, which are used for naming esoteric languages. I neither support nor endorse this, it's just what it is.

In my previous post, I showed how we can do looping transactions on the Bitcoin Cash blockchain. Unfortunately, this caused some confusion, as people initially believed OP_CHECKDATASIG allowed to loop within Bitcoin Script.

This is not true, with the scheme I showed we can only loop using a new transaction for each loop. People claiming CSW was right about OP_CHECKDATASIG introducing loops IN Script are wrong. It's just false. The idea that you could call another transaction by checking a signature is just ludicrous.

To keep the loops spinning, you have to feed it with more Satoshis each loop, which is similar to Ethereums Gas limit. So that's fine!

Onto our next challenge: Proving turing completeness!

In that post I claimed that "we've just accidentally proved Script to be Turing complete" but didn't add any rigorous proof, other than something along the lines of "Look, it's a loop!".

It's actually pretty easiy for something to become Turing complete, and we have to be extra careful not to accidentially make something Turing complete. Conway's Game Of Life is Turing complete, as is the Rule 110 cellular automaton. The creator of Bitcoin Script was very careful not to make it Turing complete, so that it would be easy to check the script's execution costs.

A simple way to show Turing completeness is by simulating a Turing machine. For that, we'll pick a derivative of Smallfuck, an esoteric programming language, which has been shown to be Turing complete. If we can simulate that on Bitcoin, we know it's Turing complete.

Smallfuck

Smallfuck has a tape with bits (0 or 1) and a pointer that points to one of those bits. The pointer can be moved and flip bits around.

It has the following commands:

  1. > Move pointer right
  2. < Move pointer left
  3. * Flip current cell
  4. [ Jump past matching ] if cell under pointer is 0
  5. ] Jump back to matching [

I'll change the rules slightly to make it easier to implement. <position> is a 2 byte number in little endian, and _ is some arbitrary, ignored, byte:

  1. >__ Move pointer right
  2. <__ Move pointer left
  3. *__ Flip current cell
  4. [<position> Jump to <position> if cell under pointer is 0
  5. ]<position> Jump to <position> unconditionally

This actually makes the language more expressive and powerful, so it would also be Turing complete. To move the tape pointer, we have to step in multiples of 3 as each instruction is 3 bytes in size.

Encoding

We need four things for our turing machine:

  1. The program to be executed
  2. The instruction pointer
  3. The current tape
  4. The tape pointer

The first of those is fixed, the others potentially change after each step.

We'll put the program directly into the output script and the others will be put into the OP_RETURN part of the transaction.

Code

In my last post I showed how to do a transaction that has to spin indefinitely. For that we'll assume we have a function decomposePreimage which decomposes the preimage.

contract ForeverTheSame() {
    challenge spend(PubKey pk, Sig sig, bin preimageSerialized) {
        verify checkSig(sig, pk);
        verify checkDataSig(sig, preimageSerialized, pk);

        Preimage preimage = decomposePreimage(preimageSerialized);
        verify hash256(preimage.value . preimage.scriptCode) == preimage.hashOutputs;
    }
}

Now we can extend this to the following:

contract TuringMachine(bin program, // <-- program for the machine to execute
                       bin opReturnHeader,
                        // ^-- what we have to prepend to our machine state for OP_RETURN
                       bin hashSendTheMoneyBack
                        // ^-- output script for claiming the machines money once finished
                       ) {
    challenge step(PubKey pk,
                   Sig sig,
                   bin preimageSerialized,
                   bin previousTxBefore,
                   bin instructionPointer,
                   bin tapePointer,
                   bin tape,
                   bin previousTxAfter,
                   bin previousTxOutpointIndex) {
        verify checkSig(sig, pk);
        verify checkDataSig(sig, preimageSerialized, pk);

        Preimage preimage = decomposePreimage(preimageSerialized);

        verify hash256(previousTxBefore .
                       instructionPointer . tapePointer . tape .
                       previousTxAfter) .
               previousTxOutpointIndex == preimage.outpoint;
                // ^-- need to verify OP_RETURN of the previous transaction actually
                //     matches inputs
        if (bin2num(instructionPointer) == size(program)) {
         // ^-- program terminated, can now send the money back.
            verify hashSendTheMoneyBack == preimage.hashOutputs;
        } else {
            bin [_, tail] = program @ instructionPointer;
            bin [instruction, _] = tail @ 3;  // <-- take the first 3 bytes
            bin [instructionCode, instructionPosition] = instruction @ 1;
               // ^-- split into 1 byte and 2 bytes
            bin [tapeBefore, tapeAfter] = tape @ bin2num(tapePointer);
            bin [value, tapeTail] = tapeAfter @ 1;
                // ^-- extract the currently pointed to value
            bin outputPreamble = preimage.value . preimage.scriptCode . 
                                 0x0000000000000000 . opReturnHeader;

            if (instructionCode == 0x3e) { // '>' encoded
                bin newInstructionPointer = num2bin(bin2num(instructionPointer) + 3, 2);
                    // ^-- move instruction pointer
                bin newTapePointer = num2bin(bin2num(tapePointer) + 1, 2); 
                    // ^-- move one to the right on the tape
                verify hash256(outputPreamble .
                               newInstructionPointer . // <-- move instruction pointer
                               newTapePointer . // <-- move tape pointer
                               tape) == preimage.hashOutputs;
            } else if (instructionCode == 0x3c) { // '<' encoded
                bin newInstructionPointer = num2bin(bin2num(instructionPointer) + 3, 2);
                bin newTapePointer = num2bin(bin2num(tapePointer) - 1, 2); 
                // ^-- move one to the right on the tape
                verify hash256(outputPreamble .
                               newInstructionPointer . // <-- move instruction pointer
                               newTapePointer . // <-- move tape pointer
                               tape) == preimage.hashOutputs;
            } else if (instructionCode == 0x2a) { // '*' encoded
                bin newInstructionPointer = num2bin(bin2num(instructionPointer) + 3, 2);
                bin newValue = value ^ 0x01;  // flip bit
                verify hash256(outputPreamble .
                               newInstructionPointer . // <-- move instruction pointer
                               tapePointer . // <-- keep tape pointer
                               tapeBefore . newValue . tapeAfter // <-- update tape
                               ) == preimage.hashOutputs;
            } else if (instructionCode == 0x5b) { // '[' encoded
                if (value = 0x00) {
                    verify hash256(outputPreamble .
                                   instructionPosition . // <-- jump instruction pointer
                                   tapePointer .
                                   tape) == preimage.hashOutputs;
                } else {
                    bin newInstructionPointer = num2bin(bin2num(instructionPointer) + 3, 2);
                    verify hash256(outputPreamble .
                                   newInstructionPointer . // <-- move instruction pointer
                                   tapePointer .
                                   tape) == preimage.hashOutputs;
                }
            } else if (instructionCode == 0x5d) { // ']' encoded
                verify hash256(outputPreamble .
                               instructionPosition . // <-- jump instruction pointer
                               tapePointer .
                               tape) == preimage.hashOutputs;
            } else {
                verify false;  // <-- invalid opcode
            }
        }
    }
}

Ouch. That's quite a bit. Let's go through it.

Verify tape, instruction pointer and tape pointer

verify hash256(previousTxBefore . instructionPointer . tapePointer . tape . previousTxAfter)
        . previousTxOutpointIndex == preimage.outpoint;

This asserts that the state of the machine - i.e. the instruction and tape pointer as well as the tape - we've been given are in fact part of the previous transaction, which should be in the OP_RETURN part.

Handle termination

if (bin2num(instructionPointer) == size(program)) { // <-- program terminated, can now send the money back.
    verify hashSendTheMoneyBack == preimage.hashOutputs;
} 

Our program might have terminated, which means the instruction pointer is right behind the program, like when the needle of a phonograph moves off the record.

Extract instruction code and tape value

bin [_, tail] = program @ instructionPointer;
bin [instruction, _] = tail @ 3;  // <-- take the first 3 bytes
bin [instructionCode, instructionPosition] = instruction @ 1;
   // ^-- split into 1 byte (instructionCode) and 2 bytes (instructionPosition)
bin [tapeBefore, tapeAfter] = tape @ bin2num(tapePointer);
bin [value, tapeTail] = tapeAfter @ 1; // <-- extract the currently pointed to value

Next we disassemble our tape and program, to find out our current instruction (i.e. its code and where to jump to, if we must) and the current value on the tape.

Build output block preamble

bin outputPreamble = preimage.value . preimage.scriptCode .
                     0x0000000000000000 . opReturnHeader;

To make life easier for us, we add a "preamble" that we prepend in front of the actual state of the machine. We'll use that below all the time. The transaction should have 2 outputs, one which continues the execution of the machine and one which encodes the state of the machine.

Case 1. >: Move tape pointer to the right

if (instructionCode == 0x3e) { // '>' encoded

We check for the case '>', ...

bin newInstructionPointer = num2bin(bin2num(instructionPointer) + 3, 2);
    // ^-- move instruction pointer

... and move the instruction pointer 3 bytes to the right. As defined above, our instructions all take 3 bytes. After we execute an instruction, we move over to the next after that.

bin newTapePointer = num2bin(bin2num(tapePointer) + 1, 2); 
    // ^-- move one to the right on the tape

We then move the tape pointer one to the right, as defined in the instruction.

verify hash256(outputPreamble .
               newInstructionPointer . // <-- move instruction pointer
               newTapePointer . // <-- move tape pointer
               tape) == preimage.hashOutputs;

And finally, we verify that the transaction actually moves the instruction pointer and tape pointer to the right.

Case 2. <: Move tape pointer to the left

This is the same as Case 2, only that the tape pointer is moved to the left instead.

bin newTapePointer = num2bin(bin2num(tapePointer) - 1, 2); 

Case 3. *: Flip the bit under the instruction pointer

As before, we move the instruction pointer to the right:

bin newInstructionPointer = num2bin(bin2num(instructionPointer) + 3, 2);

Then we flip the bit at the current tape position (extracted above), ...

bin newValue = value ^ 0x01;  // flip bit

... and verify the transaction actually flips the bit.

verify hash256(outputPreamble .
               newInstructionPointer . // <-- move instruction pointer
               tapePointer . // <-- keep tape pointer
               tapeBefore . newValue . tapeAfter // <-- update tape
               ) == preimage.hashOutputs;

Case 4. [: Jump conditionally

For this we have two cases:

if (value = 0x00) {
    verify hash256(outputPreamble .
                   instructionPosition . // <-- jump instruction pointer
                   tapePointer .
                   tape) == preimage.hashOutputs;
}

The first case happens when the current value is 0. Then we jump to the position defined in the instruction, instructionPosition, which we've extracted earlier.

The other case:

else {
    bin newInstructionPointer = num2bin(bin2num(instructionPointer) + 3, 2);
    verify hash256(outputPreamble .
                   newInstructionPointer . // <-- move instruction pointer
                   tapePointer .
                   tape) == preimage.hashOutputs;
}

Here, we just move the instruction pointer one to the right.

Case 5. ]: Jump unconditionally

And finally, the unconditional branch. It's the same as in the first branch of the previous instruction code.

verify hash256(outputPreamble .
               instructionPosition . // <-- jump instruction pointer
               tapePointer .
               tape) == preimage.hashOutputs;

Conclusion

We've shown that it's possible to build a Turing machine, Smallfuck to be precise, on top of Bitcoin, using OP_CHECKDATASIG and one transaction for each execution step. This means Bitcoin is indeed Turing complete!

But there's more. We could optimize the current code we have to get a real, fully fledged and operational Bitcoin Virtual Machine. It could be very similar to Ethereum's EVM, but with something much better than a gas limit, a pay-for-each-execution-step model. The possibilities this offers are myriad.