Skip to content

Building scripts with the Tape API

Libs edited this page May 11, 2022 · 1 revision

Imagine a Bitcoin Script is like a tape, where each cell of the tape contains either some data (a vector of bytes), or an instruction (a single byte known as an Opcode).

When the tape is played back, each cell is evaluated from start to finish. If a cell contains a piece of data, that data is placed onto a "stack". If a cell contains an Opcode, then an instruction is carried out on the stack. Some Opcodes manipulate the order of the stack, some take one or more values from the stack as inputs and replace them with a computed output.

Bitcoin Script is a predicate function. This means it evaluates as true or false. When the tape has finished, if the top item of the stack contains a null value (an empty byte vector or a vector of zeros), then it evaluates as false. Otherwise it evaluates as true and the script is considered valid.

When a Bitcoin Script is truthy, it is a valid script and coins are about to be spent.

Tape class

In the Cast lockingScript() and unlockingScript() functions, this.script is an instance of Tape, which provides and API for building our script. Tape functions always return this can be chained together.

push(data)

Pushes a data value onto the tape. Accepts any of a Opcode integer, byte vector or string. If an array of valid values is given, each element is pushed onto the tape. Numbers should be encoded using the num() helper.

this.script
  .push('hello world')
  .push(['hello', 'world'])
  .push(num(24))

// script evaluates to ["hello world", "hello", "world", [24]]

apply(macroFn, args = [])

Calls the macro function with the given args, and the Cast instance applied the this context. Macros are useful for breaking up more complex sequences of script into re-usable functions.

function reverse(len) {
  for (let i = 1; i < len; i++) {
    this.script.push(OP_1).push(OP_SPLIT)
  }

  for (let i = 1; i < len; i++) {
    this.script.push(OP_SWAP).push(OP_CAT)
  }
}

this.script
  .push([1, 2, 3, 4])
  .apply(reverse, 4)

// script evaluates to [[4, 3, 2, 1]]

each(items, callbackFn)

Iterates over the given elements invoking the callback on each. The callback will be invoked with the Cast instance as the this context.

this.script
  .each(['foo', 'bar'], (el, i) => {
    this.script.push(el).push(num(i))
  })

// script evaluates to ["foo", OP_0, "bar", OP_1]

repeat(n, callbackFn)

Iterates the given number of times invoking the callback on each loop. The callback will be invoked with the Cast instance as the this context.

this.script
  .repeat(3, (i) => {
    tape.push(OP_1)
    tape.push(OP_SPLIT)
  })

// script evaluates to [OP_1, OP_SPLIT, OP_1, OP_SPLIT, OP_1, OP_SPLIT]