TurtleVm: A meta-circular evaluator for Bitcoin Cash Script (Proof of Concept)

TurtleVm is an evaluator for Bitcoin Cash Script (2025 & beyond) implemented in Bitcoin Cash Script (2025). It currently supports all the opcodes except: OP_PUSHDATA_X, OP_TOALTSTACK, OP_FROMALTSTACK, OP_CODESEPARATOR, OP_ACTIVEBYTECODE. It uses the primary stack of the underlying Bitcoin Cash VM and is therefore subject to the same limits on the size of the stack entries. It has a separate control stack with configurable size. TurtleVm is a Proof of Concept and is not fully developed and tested.

The main purpose of TurtleVm is to illustrate this point:

In practice, the difference between code and data in Bitcoin Cash Script is not clear cut.

Today P2SH contracts can accept data (byte strings) as input. They can transform/mutate data using several opcodes. Using the conditional branching opcodes, contracts can let data affect the control flow in the contract. Data affecting the control flow sounds a bit like code. If the data is made to be byte strings of Bitcoin Cash opcodes, then it also resembles code. If the contract can arrange its control flow based on these “byte strings of opcodes” in a similar way as the Bitcoin Cash VM would, so that the effect on the primary stack is the same, then the similarity with code is complete. This is how TurtleVm operates.

Due to lack of loops in BCH2025, TurtleVm’s opcode evaluator must be unrolled. Therefore TurtleVm grows with the length of the opcode sequences it should be able to evaluate. This makes it impractical/impossible to use the full TurtleVm in Mainnet transactions. However, it is possible to create versions of TurtleVm that can evaluate short programs consisting of a very limited subset of opcodes. One such limited version is MiniTurtleVm-101 which is small enough to be relayed in a transaction on Mainnet.

MiniTurtleVm-101 has the following properties:

  • P1: It can evaluate a trace of a maximum of 12 opcodes.

  • P2: It supports these opcodes:

    • OP_DATA_02 (0x02): ( – {two-byte string} )

    • OP_1 (0x51): ( – 1)

    • OP_1ADD (0x8b): ( x1 – {x1 + 1} )

    • OP_MUL (0x95): ( x1 x2 – {x1 * x2} )

    • OP_DEFINE (0x89): ( {bytes} – )

    • OP_INVOKE (0x8a): ( – {result of function evaluation} )

    • As seen above, MiniTurtleVm-101 supports OP_DEFINE/OP_INVOKE (limited versions); opcodes that are not present in the implementation language (Bitcoin Cash Script (2025)).

  • P3: The default OP_DEFINE function at VM startup is “OP_0”.

A contract using MiniTurtleVm-101 has been deployed in a P2SH transaction at:

bitcoincash:pzf0mthtyfnm8fmma9ew9n59282azuq4egu6hx6slq

with BCH 0.02 (~1 beer or so) in funds. The contract will release the funds to anyone who can provide a MiniTurtleVm-101 program satisfying the following conditions (or who can utilize any bugs in the contract):

  • C1: The program is a maximum of 9 bytes long.

  • C2: The program leaves the value 5 on the stack when finished (and nothing else).

  • C3: The program does not use the same byte/opcode twice in a row.

  • C4: The program does not use OP_1ADD right after OP_MUL.

The contract redeem script (which contains MiniTurtleVm-101) is:

8259a1697601ff7c768251a27763517f527952798791697b0195876378018b879169686768768251a27763517f527952798791697b0195876378018b879169686768768251a27763517f527952798791697b0195876378018b879169686768768251a27763517f527952798791697b0195876378018b879169686768768251a27763517f527952798791697b0195876378018b879169686768768251a27763517f527952798791697b0195876378018b879169686768768251a27763517f527952798791697b0195876378018b879169686768768251a27763517f527952798791697b0195876378018b879169686768768251a27763517f527952798791697b0195876378018b8791696867686d01006b6b6c82009c636b5267517f6b517c7e68517f7c51876301007e815167750000686376529c63756c527f6b677601519c6375516776028b009c63758b67760289009c63756c6c757c6b6b6776028a009c63756c6c767b7e7c6b6b67760295009c6375956702453100696868686868686775686c82009c636b5267517f6b517c7e68517f7c51876301007e815167750000686376529c63756c527f6b677601519c6375516776028b009c63758b67760289009c63756c6c757c6b6b6776028a009c63756c6c767b7e7c6b6b67760295009c6375956702453100696868686868686775686c82009c636b5267517f6b517c7e68517f7c51876301007e815167750000686376529c63756c527f6b677601519c6375516776028b009c63758b67760289009c63756c6c757c6b6b6776028a009c63756c6c767b7e7c6b6b67760295009c6375956702453100696868686868686775686c82009c636b5267517f6b517c7e68517f7c51876301007e815167750000686376529c63756c527f6b677601519c6375516776028b009c63758b67760289009c63756c6c757c6b6b6776028a009c63756c6c767b7e7c6b6b67760295009c6375956702453100696868686868686775686c82009c636b5267517f6b517c7e68517f7c51876301007e815167750000686376529c63756c527f6b677601519c6375516776028b009c63758b67760289009c63756c6c757c6b6b6776028a009c63756c6c767b7e7c6b6b67760295009c6375956702453100696868686868686775686c82009c636b5267517f6b517c7e68517f7c51876301007e815167750000686376529c63756c527f6b677601519c6375516776028b009c63758b67760289009c63756c6c757c6b6b6776028a009c63756c6c767b7e7c6b6b67760295009c6375956702453100696868686868686775686c82009c636b5267517f6b517c7e68517f7c51876301007e815167750000686376529c63756c527f6b677601519c6375516776028b009c63758b67760289009c63756c6c757c6b6b6776028a009c63756c6c767b7e7c6b6b67760295009c6375956702453100696868686868686775686c82009c636b5267517f6b517c7e68517f7c51876301007e815167750000686376529c63756c527f6b677601519c6375516776028b009c63758b67760289009c63756c6c757c6b6b6776028a009c63756c6c767b7e7c6b6b67760295009c6375956702453100696868686868686775686c82009c636b5267517f6b517c7e68517f7c51876301007e815167750000686376529c63756c527f6b677601519c6375516776028b009c63758b67760289009c63756c6c757c6b6b6776028a009c63756c6c767b7e7c6b6b67760295009c6375956702453100696868686868686775686c82009c636b5267517f6b517c7e68517f7c51876301007e815167750000686376529c63756c527f6b677601519c6375516776028b009c63758b67760289009c63756c6c757c6b6b6776028a009c63756c6c767b7e7c6b6b67760295009c6375956702453100696868686868686775686c82009c636b5267517f6b517c7e68517f7c51876301007e815167750000686376529c63756c527f6b677601519c6375516776028b009c63758b67760289009c63756c6c757c6b6b6776028a009c63756c6c767b7e7c6b6b67760295009c6375956702453100696868686868686775686c82009c636b5267517f6b517c7e68517f7c51876301007e815167750000686376529c63756c527f6b677601519c6375516776028b009c63758b67760289009c63756c6c757c6b6b6776028a009c63756c6c767b7e7c6b6b67760295009c637595670245310069686868686868677568559d51

The contract takes a single argument which is the MiniTurtleVm-101 program to evaluate.

The above P2SH contract illustrates that today’s contracts can accept “code” as input. They can also perform “code” mutation.

All of the above ties into the risk discussions around “CHIP-2025-05 Functions”. I’m for the Functions CHIP in its current formulation: https://github.com/bitjson/bch-functions (3c40074).

I’ll push the source code as part of the albaDsl project later.

6 Likes

Very creative - fun demo too (I paid it back to the same address if others want to have a go - it’s a good exercise).

FWIW, I’m also leaning in support of the CHIP-2025-05 Functions CHIP. And I think MiniTurtleVM is a very good counter to the argument against the Functions CHIP on the grounds that a Contract Dev might do something unsafe (e.g. running a program by introspecting a token commitment program):

  1. There are many other ways for a contract dev to shoot themselves in the foot anyway.
  2. MiniTurtleVM demonstrates that, if they really wanted to, they’d still be able to do it anyway (and probably with a far larger program size once we get looping?)
5 Likes

Cool that you already solved it! Your solution is 9 bytes. I was aware of this solution. I’m aware of a couple of shorter solutions too :).

Yes, there are already countless ways to shoot yourself in the foot. Every time that we extend the VM they multiply. It is a natural consequence of the script language gaining in power. Higher level languages easily rule them out by offering the right constructs to developers.

Yes, with loops there is no restriction on the size of programs that TurtleVm can evaluate, apart from the normal VM limits which all contracts are subject to.

5 Likes

I think I know one of them (a “useless” define approach), but I felt like I would’ve been cheating :sweat_smile:

1 Like

The shortest solution is nice but a bit of a trick solution since it relies on limitations in MiniTurtleVm-101.

1 Like

I published the code for TurtleVm and the MiniTurtleVM Challenge contract.

TurtleVm implementation:
https://github.com/albaDsl/alba-dsl/tree/main/src/DslDemo/TurtleVm

MiniTurtleVm101 main entry point:
https://github.com/albaDsl/alba-dsl/blob/main/src/DslDemo/TurtleVm/MiniTurtleVm101.hs

Challenge contract:
https://github.com/albaDsl/alba-dsl/blob/main/apps/contracts/miniTurtleChallenge/Contract.hs

Jimtendo’s solution:
https://github.com/albaDsl/alba-dsl/blob/9a8aa9f271fc407e20fc4fcaf5d49824fd2c7034/apps/contracts/miniTurtleChallenge/Spend.hs#L43

3 Likes

I like this work, I love people actually showing up and proving things one way or another with actual running code. Cool!

I think the latest state of the functions chip was not really in conflict with the statement you saw. So while useful to have this proven, I think it was clear to many already.

The latest state was about how the default, most used way of doing things is including a clear demand that all the code used to unlock the script is known at the time of locking the funds on the chain.
So there has to be a simple opcode that does it like that.

Naturally, people should be free to shoot themselves in the foot with low level code, because you can’t avoid such hurt if you intent to provide powerful commands.
So a second opcode that doesn’t validate the script may be useful for those minor usecases. And in case people react to the word “minor”, remember that the innovation that is shown here by TurtleVM is novel, is new and smart. So after 15 years it not being widely used is a good indication that this way of doing things is not the most used way.
Sure, we may want to support it. Why not?
But bypassing verification is not something that the majority of the usages will want to do. It wastes bytes and creates opportunities for mistakes.

As stated by @albaDsl TurtleVM isn’t even possible/practical without loops and the mini variants used in the example is only possible since the VM Limits upgrade a little over 2 months ago. It has not been used at all during the last 15 years because it isn’t possible.

1 Like

On that note actually, @albaDsl assuming looping, any idea (approximately) how many bytes the full TurtleVM would use?

With loops and with the current feature set I think it would be in the 1750 - 2000 byte range. If we also rely on OP_DEFINE/OP_INVOKE we can save a few bytes here and there. Also, with OP_DEFINE/OP_INVOKE one could assign TurtleVm (or part of it) to a function slot and use it as an inefficient OP_EVAL. Possibly too inefficient to be useful in practice though.

Implementing TurtleVm on top of the suggested BCH2026 opcodes is something I plan to explore.

2 Likes

Thanks Tom!

I’ll comment with my view on using script hashes in OP_DEFINE later.

I made a version of turtleVm implemented on top of Bitcoin Cash Script (2026) (Loops & Functions). It has the following properties:

  • It supports all BCH 2025 opcodes except: OP_PUSHDATA_X, OP_CODESEPARATOR, OP_ACTIVEBYTECODE. The alt stack operations are now implemented.
  • It is 1103 bytes large (there is room for more optimizations).
  • It consists of 139 functions (almost all of them are part of a dispatch table).
  • It can evaluate any size program within the VM limits.

If only loops had been used in the implementation, then this turtleVm would have been in the 1750 - 2000 byte range as suggested previously. But by using functions the size could be brought down to 1103.

The dispatch table is very simple. For example, in function slot 0x95 we define a function that just invokes OP_MUL (0x95). This way we can dispatch directly to the handler and bypass a lot of nested OP_IFs. I think dispatch tables like this can be useful in other contexts than turtleVm too.

TurtleVm 2026 shows the power of loops and functions. The previous implementation, TurtleVm 2025, required minimally ~1750 bytes per opcode evaluated. TurtleVm 2026 does not grow in size with the number of opcodes to evaluate, and is also smaller. Apart from compressing code, functions also offer a means of abstraction and the ability to do recursion.

I wanted to try TurtleVm 2026 on some larger program. So I tested having it evaluate MiniTurtleVm-101, as MiniTurtleVm-101 in turn is evaluating @jimtendo’s solution to the MiniTurtleVm-101 challenge. It produces the correct result (5) so I suppose it works :). (Example 9)

If each of the 139 OP_DEFINEs had been required to also be accompanied by a 32 byte hash of the code, then the above solution would not have been feasible.

2 Likes

This is very clever!

So I tested having it evaluate MiniTurtleVm-101, as MiniTurtleVm-101 in turn is evaluating @jimtendo’s solution to the MiniTurtleVm-101 challenge. It produces the correct result (5) so I suppose it works :slight_smile:

image :sweat_smile:

I was thinking though, even when we have native OP_DEFINE, a potential use-case for this could be to safely execute arbitrary scripts that exist in a token commitment, etc, by limiting the OP_CODEs and also maybe stack depth (at point of TurtleVM execution)? Not sure if that could work in practice, but am thinking of those cases where we might want something akin to a callback that can’t modify existing items on the stack:

someCallbackThatIsOnATokenCommitment = (dataIn) => {
 // Execute but disallow modification of existing Stack.
 return newStackItems;
}

So, every OP_PUSH increments some internal Stack Counter and every OP_DROP decrements it. If something attempts to go below this counter, an error is thrown. Not really sure whether this would be feasible to track in practice though - and I imagine other Stack Manipulation OPs would be very tricky to manage.

If we end up with something like @bitjson is proposing here: CHIP-2025-01 TXv5: Transaction Version 5 - #17 by bitjson

Further, Bitcoin Cash covenants are better positioned to safely adopt new technologies: other networks require extensive development, testing, and advocacy prior to the deployment of upgrades, while equally-efficient Bitcoin Cash covenants can be deployed immediately, at negligible cost, without risk to the wider Bitcoin Cash network, and with risks precisely controlled by the end user (i.e. only the minimal assets trusted to a new system can be lost by a vulnerability in that system).

The program for this could even be stored on-chain and cheaply leveraged by contracts that need it.

1 Like

INCEPTION :sweat_smile:

It is turtles all the way down! (Wikipedia). Or in our case, up. E0 is the evaluator built into the node (albaVm here), E1 is TurtleVm 2026, and E2 is MiniTurtleVm-101. And the code is the 9-byte challenge solution. E0 (E1 (E2 code)).

Yes, practicality aside, I think what TurtleVm could offer beyond native OP_DEFINE/INVOKE is a sandbox within the sandbox. It also can evaluate code without using up additional function slots (similar to OP_EVAL). I think TurtleVm could keep track of the stack depth as you suggest. Ordinary ops like OP_MUL would also need to do this accounting. It would grow TurtleVm a bit and make it a bit less efficient.

TurtleVm is very inefficient as is. The number of opcodes evaluations required to evaluate some snippet of code on E0 multiplies when evaluating on TurtleVm on E0. It could be optimized a bit in this regard. Also, if we get a reduction in the VM-limits cost per evaluated opcode then TurtleVm becomes more feasible to use.

The program for this could even be stored on-chain and cheaply leveraged by contracts that need it.

You mean that if you need a sandboxed evaluator, it could be stored in a read-only input for re-use?

Although evaluating code from outside the contract gets a lot of attention, I want to reiterate that in my view the main benefits of the Functions proposal lie in structuring ordinary self-contained contracts as they grow in complexity. And those benefits are huge.

1 Like

Yeah, exactly! Not certain I’m entirely on-board with the concept of read-only inputs yet (they do make me a little nervous because, behaviourally, they feel a little bit exceptional). But I think it’s good to make sure we leave the door open for these kinds of ideas. Otherwise, if we place unnecessary constraints on things (e.g. design such that introspective eval’s can’t be done), we might inadvertently kill a good future use-case.

the main benefits of the Functions proposal lie in structuring ordinary self-contained contracts as they grow in complexity. And those benefits are huge.

Agree 100%.

1 Like

I like read-only inputs. They are meant to have their lock script evaluated and succeed though. But the lock script could do nothing else than verify that it is used read-only, while also providing the shared code in a non-executed code path. Then other inputs could dynamically link in code from it using e.g. OP_INPUTBYTECODE.

This kind of setup has similarities with dynamic linking of shared libraries. But here the library sits on the blockchain, is protected by a transaction hash / PoW, and its reputation can be measured by how much use it gets in transactions. One could even calculate its code coverage from the overall transaction history and perhaps associate a monetary value with each covered code path.

1 Like

I’m not sure we can use OP_INPUTBYTECODE - at least, not with the compression benefits because I think it means we’d just be re-providing the program (as Unlocking Bytecode) each time we use that input? (Maybe I’ve missed a trick here though?)

But, with the P2S CHIP, I think we would get ~340 byte program chunks we can use per input:

  1. OP_OUTPUTBYTECODE / Locking Bytecode = 220 Bytes
  2. OP_UTXOTOKENCOMMITMENT / Token Commitment = 120 Bytes

This is still a pretty significant shaving in my opinion:

  1. I think it would probably cost a bit more than 40 bytes up front (txid covenant and then combining them into a singular program) + ~40 bytes per input/progam used (including them as an input). Note that if we know the outpoint id and the input is provably unspendable, I don’t think we need to verify the hash of the program itself - just have to setup the covenant to check the OP_OUTPOINTTXHASH matches the expected.
  2. If we assume roughly 80 bytes per input (I think it’ll be cheaper than this?) and each input can carry a 340 byte program, then the savings are still more than 4x versus inlining these programs in our Unlocking Bytecode.

^ My Math might be way off… I haven’t actually PoC’d the above

I was referring to linking in code from the lock script of the read-only input so I meant to say OP_UTXOBYTECODE. But now I see you referring to OP_OUTPUTBYTECODE in your comment which has me confused because the lock script of the read-only input does not get propagated in an output. :sweat_smile:

But anyway, I agree that this can reduce transaction sizes among other benefits, and is worthwhile to explore. I think I’ll look into “dynamic linking”.

1 Like

OP_OUTPUTBYTECODE

Oh, I fucked up too! Yeah, I meant that one! (OP_UTXOBYTECODE).