Class: BSV::Script::Script

Inherits:
Object
  • Object
show all
Defined in:
lib/bsv/script/script.rb

Overview

A Bitcoin script — a sequence of opcodes and data pushes.

Scripts are the programmable spending conditions attached to transaction outputs (locking scripts) and inputs (unlocking scripts). This class provides construction from multiple formats, type detection, data extraction, and template constructors for standard script types.

Follows the SDK's "recognise everything, construct only what's valid" principle — detection methods (e.g. p2sh?) work for all script types, but constructors are only provided for types valid on BSV.

Examples:

Build a P2PKH locking script

script = BSV::Script::Script.p2pkh_lock(pubkey_hash)
script.type #=> "pubkeyhash"

Parse from hex and inspect

script = BSV::Script::Script.from_hex('76a914...')
script.p2pkh? #=> true
script.to_asm #=> "OP_DUP OP_HASH160 ... OP_EQUALVERIFY OP_CHECKSIG"

Constant Summary collapse

RPUZZLE_HASH_OPS =

Hash type to opcode mapping for RPuzzle scripts.

{
  raw: nil,
  sha1: Opcodes::OP_SHA1,
  ripemd160: Opcodes::OP_RIPEMD160,
  sha256: Opcodes::OP_SHA256,
  hash160: Opcodes::OP_HASH160,
  hash256: Opcodes::OP_HASH256
}.freeze
RPUZZLE_OP_TO_TYPE =

Reverse lookup: opcode → hash type symbol (excludes :raw).

RPUZZLE_HASH_OPS.except(:raw).invert.freeze
RPUZZLE_PREFIX =

The fixed opcode prefix shared by all RPuzzle locking scripts. OP_OVER OP_3 OP_SPLIT OP_NIP OP_1 OP_SPLIT OP_SWAP OP_SPLIT OP_DROP

[
  Opcodes::OP_OVER, Opcodes::OP_3, Opcodes::OP_SPLIT,
  Opcodes::OP_NIP, Opcodes::OP_1, Opcodes::OP_SPLIT,
  Opcodes::OP_SWAP, Opcodes::OP_SPLIT, Opcodes::OP_DROP
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(bytes = ''.b) ⇒ Script

Returns a new instance of Script.

Parameters:

  • bytes (String) (defaults to: ''.b)

    raw script bytes (default: empty)



33
34
35
36
37
38
39
40
# File 'lib/bsv/script/script.rb', line 33

def initialize(bytes = ''.b)
  # Defensive copy + freeze: scripts are immutable in the protocol; freezing
  # @bytes prevents external in-place mutation from staling caches that
  # depend on the script's serialisation (see
  # docs/reference/sighash-cache.md).
  @bytes = bytes.b.dup.freeze
  @chunks = nil
end

Instance Attribute Details

#bytesString (readonly)

Note:

Scripts are protocol-level immutable; the underlying bytes are defensively copied + frozen on construction. Downstream callers that need to build a script incrementally must compose bytes into a new String and pass it to a fresh Script.new.

Returns the raw script bytes (frozen, binary encoding).

Returns:

  • (String)

    the raw script bytes (frozen, binary encoding)



30
31
32
# File 'lib/bsv/script/script.rb', line 30

def bytes
  @bytes
end

Class Method Details

.builderBuilder

Create a new Builder for fluent script construction.

Returns:



135
136
137
# File 'lib/bsv/script/script.rb', line 135

def self.builder
  Builder.new
end

.from_asm(asm_string) ⇒ Script

Parse a script from ASM notation.

Opcodes are given by name (e.g. "OP_DUP"), data pushes as hex. Supports the canonical aliases "0" (OP_0) and "-1" (OP_1NEGATE). Explicit PUSHDATA sequences (+OP_PUSHDATA1 +, etc.) are consumed as a unit.

Parameters:

  • asm_string (String)

    space-separated ASM tokens

Returns:



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/bsv/script/script.rb', line 67

def self.from_asm(asm_string)
  buf = ''.b
  tokens = asm_string.split
  i = 0
  while i < tokens.length
    token = tokens[i]

    # Canonical short-hand aliases
    if token == '0'
      buf << [Opcodes::OP_0].pack('C')
      i += 1
      next
    end

    if token == '-1'
      buf << [Opcodes::OP_1NEGATE].pack('C')
      i += 1
      next
    end

    opcode = resolve_opcode(token)

    if [Opcodes::OP_PUSHDATA1, Opcodes::OP_PUSHDATA2, Opcodes::OP_PUSHDATA4].include?(opcode)
      # Explicit PUSHDATA sequence: OP_PUSHDATAn <len> <hex>
      # Consume the following length token and hex token as a unit.
      raise ArgumentError, "#{token} requires <length> <hex> tokens" unless tokens[i + 1] && tokens[i + 2]

      hex_token = tokens[i + 2]
      data = BSV::Primitives::Hex.decode(hex_token.to_s, name: 'ASM PUSHDATA hex token')
      max_len = { Opcodes::OP_PUSHDATA1 => 0xFF, Opcodes::OP_PUSHDATA2 => 0xFFFF, Opcodes::OP_PUSHDATA4 => 0xFFFFFFFF }
      raise ArgumentError, "data too long for #{token}: #{data.bytesize} > #{max_len[opcode]}" if data.bytesize > max_len[opcode]

      case opcode
      when Opcodes::OP_PUSHDATA1
        buf << ([Opcodes::OP_PUSHDATA1, data.bytesize].pack('CC') + data)
      when Opcodes::OP_PUSHDATA2
        buf << ([Opcodes::OP_PUSHDATA2].pack('C') + [data.bytesize].pack('v') + data)
      when Opcodes::OP_PUSHDATA4
        buf << ([Opcodes::OP_PUSHDATA4].pack('C') + [data.bytesize].pack('V') + data)
      end
      i += 3
    elsif opcode
      buf << [opcode].pack('C')
      i += 1
    else
      # Data push — token is hex
      data = BSV::Primitives::Hex.decode(token, name: 'ASM hex token')
      buf << encode_push_data(data)
      i += 1
    end
  end
  new(buf)
end

.from_binary(binary) ⇒ Script

Parse a script from raw binary bytes.

Parameters:

  • binary (String)

    raw script bytes

Returns:



46
47
48
# File 'lib/bsv/script/script.rb', line 46

def self.from_binary(binary)
  new(binary)
end

.from_chunks(chunks) ⇒ Script

Build a script from an array of Chunk objects.

Parameters:

  • chunks (Array<Chunk>)

    script chunks

Returns:



125
126
127
128
129
130
# File 'lib/bsv/script/script.rb', line 125

def self.from_chunks(chunks)
  buf = chunks.map(&:to_binary).join
  script = new(buf)
  script.instance_variable_set(:@chunks, chunks.dup)
  script
end

.from_hex(hex) ⇒ Script

Parse a script from a hex string.

Parameters:

  • hex (String)

    hex-encoded script

Returns:



54
55
56
# File 'lib/bsv/script/script.rb', line 54

def self.from_hex(hex)
  new(BSV::Primitives::Hex.decode(hex, name: 'script hex'))
end

.op_cat_lock(expected_data) ⇒ Script

Construct an OP_CAT locking script.

The script concatenates two stack items and compares the result against the expected data. The spender must push two values whose concatenation equals expected_data.

Parameters:

  • expected_data (String)

    binary string — the expected result of concatenating the two unlocking values

Returns:



397
398
399
400
401
402
# File 'lib/bsv/script/script.rb', line 397

def self.op_cat_lock(expected_data)
  buf = [Opcodes::OP_CAT].pack('C')
  buf << encode_push_data(expected_data.b)
  buf << [Opcodes::OP_EQUAL].pack('C')
  new(buf)
end

.op_cat_unlock(data1, data2) ⇒ Script

Construct an OP_CAT unlocking script.

Pushes two data items onto the stack. The locking script's OP_CAT will concatenate them and compare against the expected value.

Parameters:

  • data1 (String)

    binary string — first item (pushed first, deeper on stack)

  • data2 (String)

    binary string — second item (pushed second, top of stack)

Returns:



412
413
414
415
416
# File 'lib/bsv/script/script.rb', line 412

def self.op_cat_unlock(data1, data2)
  buf = encode_push_data(data1.b)
  buf << encode_push_data(data2.b)
  new(buf)
end

.op_return(*data_items) ⇒ Script

Construct an OP_RETURN data carrier script.

Uses the safe OP_FALSE OP_RETURN prefix (provably unspendable).

Parameters:

  • data_items (Array<String>)

    one or more data payloads to embed

Returns:



147
148
149
150
151
# File 'lib/bsv/script/script.rb', line 147

def self.op_return(*data_items)
  buf = [Opcodes::OP_FALSE, Opcodes::OP_RETURN].pack('CC')
  data_items.each { |d| buf << encode_push_data(d.b) }
  new(buf)
end

.p2ms_lock(required, pubkeys) ⇒ Script

Construct an M-of-N bare multisig locking script.

Parameters:

  • required (Integer)

    number of required signatures (M)

  • pubkeys (Array<String>)

    array of public key byte strings (N keys)

Returns:

Raises:

  • (ArgumentError)

    if M or N is out of range



250
251
252
253
254
255
256
257
258
259
260
# File 'lib/bsv/script/script.rb', line 250

def self.p2ms_lock(required, pubkeys)
  n = pubkeys.length
  raise ArgumentError, 'm must be between 1 and n' unless required.between?(1, n)
  raise ArgumentError, 'n must be <= 16' unless n <= 16

  buf = [Opcodes::OP_1 + required - 1].pack('C')
  pubkeys.each { |pk| buf << encode_push_data(pk.b) }
  buf << [Opcodes::OP_1 + n - 1].pack('C')
  buf << [Opcodes::OP_CHECKMULTISIG].pack('C')
  new(buf)
end

.p2ms_unlock(*signatures) ⇒ Script

Construct a bare multisig unlocking script.

Parameters:

  • signatures (Array<String>)

    DER-encoded signatures with sighash bytes

Returns:



266
267
268
269
270
# File 'lib/bsv/script/script.rb', line 266

def self.p2ms_unlock(*signatures)
  buf = [Opcodes::OP_0].pack('C')
  signatures.each { |sig| buf << encode_push_data(sig.b) }
  new(buf)
end

.p2pk_lock(pubkey_bytes) ⇒ Script

Construct a Pay-to-Public-Key (P2PK) locking script.

Parameters:

  • pubkey_bytes (String)

    33-byte compressed or 65-byte uncompressed public key

Returns:

Raises:

  • (ArgumentError)

    if pubkey_bytes is not 33 or 65 bytes



228
229
230
231
232
233
234
# File 'lib/bsv/script/script.rb', line 228

def self.p2pk_lock(pubkey_bytes)
  raise ArgumentError, 'pubkey must be 33 or 65 bytes' unless [33, 65].include?(pubkey_bytes.bytesize)

  buf = encode_push_data(pubkey_bytes)
  buf << [Opcodes::OP_CHECKSIG].pack('C')
  new(buf)
end

.p2pk_unlock(signature_der) ⇒ Script

Construct a P2PK unlocking script.

Parameters:

  • signature_der (String)

    DER-encoded signature with sighash byte appended

Returns:



240
241
242
# File 'lib/bsv/script/script.rb', line 240

def self.p2pk_unlock(signature_der)
  new(encode_push_data(signature_der))
end

.p2pkh_lock(pubkey_hash_or_address) ⇒ Script

Construct a Pay-to-Public-Key-Hash (P2PKH) locking script.

Accepts either a raw 20-byte binary hash or a Base58Check address string. When given an address string, the version prefix is validated: 0x00 (mainnet) and 0x6f (testnet) are accepted; 0x05 (P2SH) is rejected with a clear error message.

Parameters:

  • pubkey_hash_or_address (String)

    20-byte binary pubkey hash, or a Base58Check address string

Returns:

Raises:

  • (ArgumentError)

    if the argument is not a valid 20-byte hash, if the address has an unrecognised prefix, or if a P2SH address is supplied

  • (BSV::Primitives::Base58::ChecksumError)

    if the address checksum is invalid



166
167
168
169
170
171
172
173
174
175
176
# File 'lib/bsv/script/script.rb', line 166

def self.p2pkh_lock(pubkey_hash_or_address)
  pubkey_hash = resolve_pubkey_hash(pubkey_hash_or_address)

  buf = [
    Opcodes::OP_DUP,
    Opcodes::OP_HASH160
  ].pack('CC')
  buf << encode_push_data(pubkey_hash)
  buf << [Opcodes::OP_EQUALVERIFY, Opcodes::OP_CHECKSIG].pack('CC')
  new(buf)
end

.p2pkh_unlock(signature_der, pubkey_bytes) ⇒ Script

Construct a P2PKH unlocking script.

Parameters:

  • signature_der (String)

    DER-encoded signature with sighash byte appended

  • pubkey_bytes (String)

    compressed or uncompressed public key bytes

Returns:



217
218
219
220
221
# File 'lib/bsv/script/script.rb', line 217

def self.p2pkh_unlock(signature_der, pubkey_bytes)
  buf = encode_push_data(signature_der)
  buf << encode_push_data(pubkey_bytes)
  new(buf)
end

.pushdrop_lock(fields, lock_script, lock_position: :before) ⇒ Script

Construct a PushDrop locking script.

Pushes arbitrary data fields onto the stack, then drops them all before the locking condition executes. Used for token protocols where data must be embedded in spendable outputs.

The lock_position parameter controls where the locking script is placed relative to the push/drop sequence:

  • :'before' (default) — lock script appears before the data pushes and drops. Matches the ts-sdk default and the canonical PushDrop layout used by overlay token protocols. Structure: [lock_script] [field0] ... [fieldN] [OP_2DROP...] [OP_DROP?]
  • :'after' — lock script appears after the drops (legacy behaviour). Structure: [field0] ... [fieldN] [OP_2DROP...] [OP_DROP?] [lock_script]

Breaking change (v0.9): the default changed from :'after' to :'before' to match the ts-sdk. Callers that relied on the old default must pass lock_position: :after explicitly.

Parameters:

  • fields (Array<String>)

    data payloads to embed (binary strings)

  • lock_script (Script)

    the underlying locking condition (e.g. P2PKH)

  • lock_position (Symbol) (defaults to: :before)

    :before (default) or :after

Returns:

Raises:

  • (ArgumentError)

    if fields is empty, lock_script is not a Script, or lock_position is not :before or :after



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/bsv/script/script.rb', line 298

def self.pushdrop_lock(fields, lock_script, lock_position: :before)
  raise ArgumentError, 'fields must not be empty' if fields.empty?
  raise ArgumentError, 'lock_script must be a Script' unless lock_script.is_a?(Script)
  raise ArgumentError, "lock_position must be :before or :after, got #{lock_position.inspect}" \
    unless %i[before after].include?(lock_position)

  field_chunks = fields.map { |f| encode_minimally(f.b) }

  drop_chunks = []
  remaining = fields.length
  while remaining > 1
    drop_chunks << Chunk.new(opcode: Opcodes::OP_2DROP)
    remaining -= 2
  end
  drop_chunks << Chunk.new(opcode: Opcodes::OP_DROP) if remaining == 1

  all_chunks = if lock_position == :before
                 lock_script.chunks + field_chunks + drop_chunks
               else
                 field_chunks + drop_chunks + lock_script.chunks
               end

  from_chunks(all_chunks)
end

.pushdrop_unlock(unlock_script) ⇒ Script

Construct a PushDrop unlocking script.

Pass-through wrapper — the data fields are dropped during execution, so the unlocking script just needs to satisfy the underlying lock.

Parameters:

  • unlock_script (Script)

    unlocking script for the underlying condition

Returns:



330
331
332
# File 'lib/bsv/script/script.rb', line 330

def self.pushdrop_unlock(unlock_script)
  unlock_script
end

.rpuzzle_lock(hash_value, hash_type: :hash160) ⇒ Script

Construct an RPuzzle locking script.

RPuzzle enables hash-puzzle-based spending where the spender proves knowledge of the ECDSA K-value (nonce) that produced a signature's R component.

Parameters:

  • hash_value (String)

    the R-value or hash of R-value to lock against

  • hash_type (Symbol) (defaults to: :hash160)

    one of :raw, :sha1, :ripemd160, :sha256, :hash160, :hash256

Returns:

Raises:

  • (ArgumentError)

    if hash_type is invalid



366
367
368
369
370
371
372
373
374
375
# File 'lib/bsv/script/script.rb', line 366

def self.rpuzzle_lock(hash_value, hash_type: :hash160)
  raise ArgumentError, "unknown hash_type: #{hash_type}" unless RPUZZLE_HASH_OPS.key?(hash_type)

  buf = RPUZZLE_PREFIX.pack('C*')
  hash_op = RPUZZLE_HASH_OPS[hash_type]
  buf << [hash_op].pack('C') if hash_op
  buf << encode_push_data(hash_value.b)
  buf << [Opcodes::OP_EQUALVERIFY, Opcodes::OP_CHECKSIG].pack('CC')
  new(buf)
end

.rpuzzle_unlock(signature_der, pubkey_bytes) ⇒ Script

Construct an RPuzzle unlocking script.

Same wire format as P2PKH: signature + public key.

Parameters:

  • signature_der (String)

    DER-encoded signature with sighash byte

  • pubkey_bytes (String)

    compressed or uncompressed public key bytes

Returns:



384
385
386
# File 'lib/bsv/script/script.rb', line 384

def self.rpuzzle_unlock(signature_der, pubkey_bytes)
  p2pkh_unlock(signature_der, pubkey_bytes)
end

Instance Method Details

#==(other) ⇒ Boolean

Returns true if both scripts have identical bytes.

Parameters:

  • other (Object)

    the object to compare

Returns:

  • (Boolean)

    true if both scripts have identical bytes



720
721
722
# File 'lib/bsv/script/script.rb', line 720

def ==(other)
  other.is_a?(self.class) && @bytes == other.bytes
end

#addresses(network: :mainnet) ⇒ Array<String>

Derive Bitcoin addresses from this script.

Currently supports P2PKH scripts only.

Parameters:

  • network (Symbol) (defaults to: :mainnet)

    :mainnet or :testnet

Returns:

  • (Array<String>)

    array of derived addresses (empty if unsupported type)



698
699
700
701
702
703
704
705
# File 'lib/bsv/script/script.rb', line 698

def addresses(network: :mainnet)
  if p2pkh?
    prefix = network == :testnet ? BSV::Primitives::PublicKey::TESTNET_PUBKEY_HASH : BSV::Primitives::PublicKey::MAINNET_PUBKEY_HASH
    [BSV::Primitives::Base58.check_encode(prefix + pubkey_hash)]
  else
    []
  end
end

#chunksArray<Chunk>

Parse the script into an array of Chunk objects.

Results are cached after first parse.

Returns:

  • (Array<Chunk>)

    the parsed chunks



714
715
716
# File 'lib/bsv/script/script.rb', line 714

def chunks
  @chunks ||= parse_chunks
end

#lengthInteger

Returns script length in bytes.

Returns:

  • (Integer)

    script length in bytes



436
437
438
# File 'lib/bsv/script/script.rb', line 436

def length
  @bytes.bytesize
end

#multisig?Boolean

Whether this is a bare multisig script.

Pattern: OP_M <pubkey1> ... <pubkeyN> OP_N OP_CHECKMULTISIG

Returns:

  • (Boolean)


570
571
572
573
574
575
576
577
# File 'lib/bsv/script/script.rb', line 570

def multisig?
  c = chunks
  return false if c.length < 3
  return false unless small_int_opcode?(c[0].opcode)
  return false unless small_int_opcode?(c[-2].opcode) && c[-1].opcode == Opcodes::OP_CHECKMULTISIG

  c[1..-3].all?(&:data?)
end

#op_cat?Boolean

Whether this is an OP_CAT puzzle script.

Pattern: OP_CAT <expected_data> OP_EQUAL

Returns:

  • (Boolean)


557
558
559
560
561
562
563
# File 'lib/bsv/script/script.rb', line 557

def op_cat?
  c = chunks
  c.length == 3 &&
    c[0].opcode == Opcodes::OP_CAT &&
    c[1].data? &&
    c[2].opcode == Opcodes::OP_EQUAL
end

#op_return?Boolean

Whether this is an OP_RETURN data carrier script.

Matches both OP_RETURN ... and OP_FALSE OP_RETURN ... forms.

Returns:

  • (Boolean)


487
488
489
490
491
# File 'lib/bsv/script/script.rb', line 487

def op_return?
  b = @bytes
  (b.bytesize.positive? && b.getbyte(0) == Opcodes::OP_RETURN) ||
    (b.bytesize > 1 && b.getbyte(0) == Opcodes::OP_FALSE && b.getbyte(1) == Opcodes::OP_RETURN)
end

#op_return_dataArray<String>?

Extract data payloads from an OP_RETURN script.

After F3.1's fix, the parser absorbs all bytes following a top-level OP_RETURN into a single raw-data chunk. This method re-parses that tail so callers receive one entry per push (including bare opcodes that appear as raw bytes in the tail).

Returns:

  • (Array<String>, nil)

    array of data pushes, or nil if not OP_RETURN



628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
# File 'lib/bsv/script/script.rb', line 628

def op_return_data
  return unless op_return?

  # Determine where the payload starts (after OP_RETURN or OP_FALSE OP_RETURN)
  start = @bytes.getbyte(0) == Opcodes::OP_RETURN ? 1 : 2

  # Parse the tail bytes as a sub-script to recover individual push items.
  # The tail was stored as a raw-data chunk by parse_chunks; parsing it again
  # yields all the contained push operations.
  #
  # IMPORTANT: disable OP_RETURN termination during re-parse — the tail
  # may legitimately contain 0x6a bytes as data, and we don't want the
  # parser to stop at them.
  tail_bytes = @bytes.byteslice(start, @bytes.bytesize - start)
  return [] if tail_bytes.nil? || tail_bytes.empty?

  tail_script = Script.new(tail_bytes)
  tail_script.send(:parse_chunks, terminate_on_op_return: false).map do |ch|
    ch.data? ? ch.data : [ch.opcode].pack('C')
  end
end

#p2pk?Boolean

Whether this is a Pay-to-Public-Key (P2PK) script.

Pattern: <pubkey> OP_CHECKSIG

Returns:

  • (Boolean)


498
499
500
501
502
503
504
505
506
# File 'lib/bsv/script/script.rb', line 498

def p2pk?
  c = chunks
  return false unless c.length == 2 && c[0].data? && c[1].opcode == Opcodes::OP_CHECKSIG

  pubkey = c[0].data
  version = pubkey.getbyte(0)
  ([0x02, 0x03].include?(version) && pubkey.bytesize == 33) ||
    ([0x04, 0x06, 0x07].include?(version) && pubkey.bytesize == 65)
end

#p2pkh?Boolean

Whether this is a Pay-to-Public-Key-Hash (P2PKH) script.

Pattern: OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG

Returns:

  • (Boolean)


447
448
449
450
451
452
453
454
455
# File 'lib/bsv/script/script.rb', line 447

def p2pkh?
  b = @bytes
  b.bytesize == 25 &&
    b.getbyte(0) == Opcodes::OP_DUP &&
    b.getbyte(1) == Opcodes::OP_HASH160 &&
    b.getbyte(2) == 0x14 &&
    b.getbyte(23) == Opcodes::OP_EQUALVERIFY &&
    b.getbyte(24) == Opcodes::OP_CHECKSIG
end

#p2sh?Boolean

Whether this is a Pay-to-Script-Hash (P2SH) script.

Detection only — P2SH is not valid on BSV, so no constructor is provided. Pattern: OP_HASH160 <20 bytes> OP_EQUAL

Returns:

  • (Boolean)


463
464
465
466
467
468
469
# File 'lib/bsv/script/script.rb', line 463

def p2sh?
  b = @bytes
  b.bytesize == 23 &&
    b.getbyte(0) == Opcodes::OP_HASH160 &&
    b.getbyte(1) == 0x14 &&
    b.getbyte(22) == Opcodes::OP_EQUAL
end

#pubkey_hashString?

Extract the 20-byte public key hash from a P2PKH script.

Returns:

  • (String, nil)

    the pubkey hash, or nil if not P2PKH



605
606
607
608
609
# File 'lib/bsv/script/script.rb', line 605

def pubkey_hash
  return unless p2pkh?

  @bytes.byteslice(3, 20)
end

#push_only?Boolean

Whether the script consists entirely of push-data operations.

A script is push-only iff every chunk's opcode is at most OP_16 (0x60). Used by the script interpreter to enforce the SIGPUSHONLY verification flag on unlock scripts.

Returns:

  • (Boolean)


478
479
480
# File 'lib/bsv/script/script.rb', line 478

def push_only?
  chunks.all? { |chunk| chunk.opcode <= Opcodes::OP_16 }
end

#pushdrop?Boolean

Whether this is a PushDrop script.

Detects both lock_position: :before and lock_position: :after layouts:

  • :before: [lock_chunks...] [field0] ... [fieldN] [OP_2DROP...] [OP_DROP?]
  • :after: [field0] ... [fieldN] [OP_2DROP...] [OP_DROP?] [lock_chunks...]

Returns:

  • (Boolean)


516
517
518
# File 'lib/bsv/script/script.rb', line 516

def pushdrop?
  pushdrop_layout ? true : false
end

#pushdrop_fieldsArray<String>?

Extract the embedded data fields from a PushDrop script.

Works for both :before and :after lock positions.

Returns:

  • (Array<String>, nil)

    array of field data, or nil if not PushDrop



673
674
675
676
677
678
# File 'lib/bsv/script/script.rb', line 673

def pushdrop_fields
  layout = pushdrop_layout
  return unless layout

  layout[:field_chunks].map { |ch| decode_minimal_push(ch) }
end

#pushdrop_lock_scriptScript?

Extract the underlying lock script from a PushDrop script.

Works for both :before and :after lock positions.

Returns:

  • (Script, nil)

    the lock script portion, or nil if not PushDrop



685
686
687
688
689
690
# File 'lib/bsv/script/script.rb', line 685

def pushdrop_lock_script
  layout = pushdrop_layout
  return unless layout

  self.class.from_chunks(layout[:lock_chunks])
end

#rpuzzle?Boolean

Whether this is an RPuzzle script.

Detects the fixed R-value extraction prefix followed by an optional hash opcode, a data push, OP_EQUALVERIFY, and OP_CHECKSIG.

Returns:

  • (Boolean)


526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
# File 'lib/bsv/script/script.rb', line 526

def rpuzzle?
  c = chunks
  # Minimum: 9 prefix + hash_data + OP_EQUALVERIFY + OP_CHECKSIG = 12
  # With hash op: 13
  return false unless c.length >= 12

  # Verify the 9-opcode prefix
  RPUZZLE_PREFIX.each_with_index do |op, i|
    return false unless c[i].opcode == op
  end

  # After prefix: optional hash op, then data push, OP_EQUALVERIFY, OP_CHECKSIG
  return false unless c[-1].opcode == Opcodes::OP_CHECKSIG
  return false unless c[-2].opcode == Opcodes::OP_EQUALVERIFY
  return false unless c[-3].data?

  # Either exactly 12 chunks (raw) or 13 chunks (with hash op)
  if c.length == 12
    true
  elsif c.length == 13
    RPUZZLE_HASH_OPS.values.compact.include?(c[9].opcode)
  else
    false
  end
end

#rpuzzle_hashString?

Extract the hash value from an RPuzzle script.

Returns:

  • (String, nil)

    the locked hash/R-value, or nil if not RPuzzle



653
654
655
656
657
# File 'lib/bsv/script/script.rb', line 653

def rpuzzle_hash
  return unless rpuzzle?

  chunks[-3].data
end

#rpuzzle_hash_typeSymbol?

Detect the hash type used in an RPuzzle script.

Returns:

  • (Symbol, nil)

    the hash type (e.g. :hash160, :raw), or nil if not RPuzzle



662
663
664
665
666
# File 'lib/bsv/script/script.rb', line 662

def rpuzzle_hash_type
  return unless rpuzzle?

  chunks.length == 12 ? :raw : RPUZZLE_OP_TO_TYPE[chunks[9].opcode]
end

#script_hashString?

Extract the 20-byte script hash from a P2SH script.

Returns:

  • (String, nil)

    the script hash, or nil if not P2SH



614
615
616
617
618
# File 'lib/bsv/script/script.rb', line 614

def script_hash
  return unless p2sh?

  @bytes.byteslice(2, 20)
end

#to_asmString

Returns human-readable ASM representation.

Returns:

  • (String)

    human-readable ASM representation



431
432
433
# File 'lib/bsv/script/script.rb', line 431

def to_asm
  chunks.map(&:to_asm).join(' ')
end

#to_binaryString

Returns a copy of the raw script bytes.

Returns:

  • (String)

    a copy of the raw script bytes



421
422
423
# File 'lib/bsv/script/script.rb', line 421

def to_binary
  @bytes.dup
end

#to_hexString

Returns hex-encoded script.

Returns:

  • (String)

    hex-encoded script



426
427
428
# File 'lib/bsv/script/script.rb', line 426

def to_hex
  @bytes.unpack1('H*')
end

#typeString

Classify the script as a standard type.

Returns:

  • (String)

    one of "empty", "pubkeyhash", "pubkey", "scripthash", "nulldata", "multisig", "pushdrop", "rpuzzle", or "nonstandard"



586
587
588
589
590
591
592
593
594
595
596
597
598
# File 'lib/bsv/script/script.rb', line 586

def type
  if @bytes.empty? then 'empty'
  elsif p2pkh? then 'pubkeyhash'
  elsif p2pk? then 'pubkey'
  elsif p2sh? then 'scripthash'
  elsif op_return? then 'nulldata'
  elsif multisig? then 'multisig'
  elsif pushdrop? then 'pushdrop'
  elsif rpuzzle? then 'rpuzzle'
  elsif op_cat? then 'opcat'
  else 'nonstandard'
  end
end