Class: BSV::Script::Interpreter

Inherits:
Object
  • Object
show all
Includes:
Operations::Arithmetic, Operations::Bitwise, Operations::Crypto, Operations::DataPush, Operations::FlowControl, Operations::Splice, Operations::StackOps
Defined in:
lib/bsv/script/interpreter/interpreter.rb,
lib/bsv/script/interpreter/operations/crypto.rb,
lib/bsv/script/interpreter/operations/splice.rb,
lib/bsv/script/interpreter/operations/bitwise.rb,
lib/bsv/script/interpreter/operations/data_push.rb,
lib/bsv/script/interpreter/operations/stack_ops.rb,
lib/bsv/script/interpreter/operations/arithmetic.rb,
lib/bsv/script/interpreter/operations/flow_control.rb

Overview

Bitcoin script interpreter implementing the post-Genesis BSV script engine.

Evaluates unlock + lock script pairs, supporting the full BSV opcode set including restored opcodes (OP_MUL, OP_LSHIFT, OP_CAT, etc.) and post-Genesis rules (OP_RETURN as early success, no script size limits).

Examples:

Evaluate scripts without transaction context

BSV::Script::Interpreter.evaluate(unlock_script, lock_script)

Verify a transaction input

BSV::Script::Interpreter.verify(
  tx: transaction, input_index: 0,
  unlock_script: input.script, lock_script: prev_output.script,
  satoshis: prev_output.satoshis
)

Defined Under Namespace

Modules: Operations

Constant Summary collapse

CONDITIONAL_OPCODES =

Conditional opcodes must be processed even in non-executing branches to maintain correct nesting depth. OP_VERIF and OP_VERNOTIF are included here because they open conditional blocks (like OP_IF/OP_NOTIF) and must be dispatched to track nesting depth even when the branch is not executing. This matches the Go SDK's IsConditional() function.

[
  Opcodes::OP_IF, Opcodes::OP_NOTIF, Opcodes::OP_ELSE, Opcodes::OP_ENDIF,
  Opcodes::OP_VERIF, Opcodes::OP_VERNOTIF
].freeze
MAX_CONDITIONAL_DEPTH =

Maximum nesting depth for OP_IF / OP_NOTIF blocks. Prevents interpreter stack overflow from deeply nested conditionals.

256
CHRONICLE_ONLY_OPCODES =

Opcodes that require Chronicle to execute. With explicit flags but without UTXO_AFTER_CHRONICLE, executing any of these raises DISABLED_OPCODE. OP_VER / OP_VERIF / OP_VERNOTIF are included because pre-Chronicle they're either reserved (OP_VER) or behave as a conditional-only NOP in non-executing branches — execution itself is disabled. Mirrors TS Spend.ts (lines ~694-709) and Go IsDisabled.

[
  Opcodes::OP_2MUL, Opcodes::OP_2DIV,
  Opcodes::OP_VER, Opcodes::OP_VERIF, Opcodes::OP_VERNOTIF
].freeze
VER_CONDITIONAL_OPCODES =

The two version-conditional opcodes — extracted to module-level constants so the interpreter hot path (every conditional dispatch / every executed chunk) doesn't allocate a fresh Array per call.

[Opcodes::OP_VERIF, Opcodes::OP_VERNOTIF].freeze
KNOWN_FLAGS =

Recognised verification flag names. Catches typos (a misspelled SIGPUSHONLLY would otherwise silently disable enforcement) and forces any new flag to be declared here before use. The set is the union of flags appearing in the canonical conformance corpus and the Bitcoin Core script_tests.json fixture, plus the witness/taproot family that downstream callers filter out before reaching the interpreter.

Set[
  'CHECKLOCKTIMEVERIFY', 'CHECKSEQUENCEVERIFY',
  'CLEANSTACK', 'DERSIG', 'DISCOURAGE_UPGRADABLE_NOPS',
  'DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM',
  'GENESIS', 'LOW_S',
  'MINIMALDATA', 'MINIMALIF',
  'NULLDUMMY', 'NULLFAIL',
  'P2SH', 'SIGHASH_FORKID', 'SIGPUSHONLY', 'STRICTENC',
  'TAPROOT',
  'UTXO_AFTER_CHRONICLE', 'UTXO_AFTER_GENESIS',
  'WITNESS'
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#astackObject (readonly)

Returns the value of attribute astack.



40
41
42
# File 'lib/bsv/script/interpreter/interpreter.rb', line 40

def astack
  @astack
end

#dstackObject (readonly)

Returns the value of attribute dstack.



40
41
42
# File 'lib/bsv/script/interpreter/interpreter.rb', line 40

def dstack
  @dstack
end

Class Method Details

.evaluate(unlock_script, lock_script, flags: nil, tx_version: nil) ⇒ Boolean

Evaluate unlock + lock scripts without transaction context.

Signature operations will always fail (no sighash available).

Examples:

Relaxed mode (post-Chronicle defaults, no malleability checks)

Interpreter.evaluate(unlock, lock)                  # flags defaults to nil

Explicit flag set

Interpreter.evaluate(unlock, lock, flags: %w[UTXO_AFTER_GENESIS CLEANSTACK])

Explicit-but-empty (pre-Genesis, pre-Chronicle, strict)

Interpreter.evaluate(unlock, lock, flags: [])       # NOT the same as nil

Parameters:

  • unlock_script (Script)

    the unlocking script

  • lock_script (Script)

    the locking script

  • flags (Array<String>, Set<String>, nil) (defaults to: nil)

    explicit verification flags (e.g. SIGPUSHONLY, CLEANSTACK, UTXO_AFTER_CHRONICLE). nil selects relaxed (post-Chronicle) mode where malleability checks are off; an empty [] array is explicit-but-empty (pre-Genesis, pre-Chronicle strict mode). Each flag string must appear in KNOWN_FLAGS — unknown names raise ArgumentError.

  • tx_version (Integer, nil) (defaults to: nil)

    transaction version made available to +OP_VER+/+OP_VERIF+/+OP_VERNOTIF+ when no transaction is supplied

Returns:

  • (Boolean)

    true if execution succeeds

Raises:

  • (ScriptError)

    if script execution fails

  • (ArgumentError)

    if flags contains an unknown name or tx_version is not a valid uint32



118
119
120
121
122
123
124
125
# File 'lib/bsv/script/interpreter/interpreter.rb', line 118

def self.evaluate(unlock_script, lock_script, flags: nil, tx_version: nil)
  new(
    unlock_script: unlock_script,
    lock_script: lock_script,
    flags: flags,
    tx_version: tx_version
  ).execute
end

.verify(tx:, input_index:, unlock_script:, lock_script:, satoshis:, flags: nil) ⇒ Boolean

Verify a transaction input by evaluating its scripts.

Parameters:

  • tx (Transaction::Tx)

    the transaction being verified

  • input_index (Integer)

    the input index within the transaction

  • unlock_script (Script)

    the input's unlocking script

  • lock_script (Script)

    the previous output's locking script

  • satoshis (Integer)

    the value of the previous output in satoshis

  • flags (Array<String>, Set<String>, nil) (defaults to: nil)

    explicit verification flags (see evaluate). Production callers (e.g. Tx#verify_input) do not pass flags, so mainnet transaction validation always runs in relaxed mode; the parameter exists for conformance-corpus and regression-vector runners that need to drive specific flag combinations.

Returns:

  • (Boolean)

    true if verification succeeds

Raises:

  • (ScriptError)

    if script execution fails

  • (ArgumentError)

    if flags contains an unknown name



142
143
144
145
146
147
148
149
150
151
# File 'lib/bsv/script/interpreter/interpreter.rb', line 142

def self.verify(tx:, input_index:, unlock_script:, lock_script:, satoshis:, flags: nil)
  new(
    unlock_script: unlock_script,
    lock_script: lock_script,
    tx: tx,
    input_index: input_index,
    satoshis: satoshis,
    flags: flags
  ).execute
end

Instance Method Details

#executeObject



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/bsv/script/interpreter/interpreter.rb', line 153

def execute
  enforce_sig_pushonly

  scripts = [@unlock_script, @lock_script]
  script_names = %w[unlock_script lock_script]

  scripts.each_with_index do |script, script_idx|
    @current_script = script
    chunks = script.chunks
    BSV.logger&.debug { "[Interpreter] === #{script_names[script_idx]} (#{chunks.length} chunks) ===" }

    chunks.each_with_index do |chunk, chunk_idx|
      @current_chunk_idx = chunk_idx
      execute_opcode(chunk)
      break if @early_return
    end

    break if @early_return && script_idx == 1

    # Between scripts: verify conditionals balanced
    raise ScriptError.new(ScriptErrorCode::UNBALANCED_CONDITIONAL, 'unbalanced conditional') unless @cond_stack.empty?

    # Clear alt stack between scripts
    @astack.clear

    # Reset state for next script
    @last_code_sep = 0
    @early_return = false
    @after_op_return = false
  end

  check_final_stack
  BSV.logger&.debug { "[Interpreter] final stack: #{@dstack.length} items -> success" }
  true
end