Class: Bitcoin::Script

Inherits:
Object show all
Includes:
Opcodes
Defined in:
lib/bitcoin/script/script.rb

Overview

bitcoin script

Constant Summary

Constants included from Opcodes

Opcodes::DUPLICATE_KEY, Opcodes::NAME_MAP, Opcodes::OPCODES_MAP, Opcodes::OP_0, Opcodes::OP_0NOTEQUAL, Opcodes::OP_1, Opcodes::OP_10, Opcodes::OP_11, Opcodes::OP_12, Opcodes::OP_13, Opcodes::OP_14, Opcodes::OP_15, Opcodes::OP_16, Opcodes::OP_1ADD, Opcodes::OP_1NEGATE, Opcodes::OP_1SUB, Opcodes::OP_2, Opcodes::OP_2DIV, Opcodes::OP_2DROP, Opcodes::OP_2DUP, Opcodes::OP_2MUL, Opcodes::OP_2OVER, Opcodes::OP_2ROT, Opcodes::OP_2SWAP, Opcodes::OP_3, Opcodes::OP_3DUP, Opcodes::OP_4, Opcodes::OP_5, Opcodes::OP_6, Opcodes::OP_7, Opcodes::OP_8, Opcodes::OP_9, Opcodes::OP_ABS, Opcodes::OP_ADD, Opcodes::OP_AND, Opcodes::OP_BOOLAND, Opcodes::OP_BOOLOR, Opcodes::OP_CAT, Opcodes::OP_CHECKMULTISIG, Opcodes::OP_CHECKMULTISIGVERIFY, Opcodes::OP_CHECKSIG, Opcodes::OP_CHECKSIGVERIFY, Opcodes::OP_CODESEPARATOR, Opcodes::OP_DEPTH, Opcodes::OP_DIV, Opcodes::OP_DROP, Opcodes::OP_DUP, Opcodes::OP_ELSE, Opcodes::OP_ENDIF, Opcodes::OP_EQUAL, Opcodes::OP_EQUALVERIFY, Opcodes::OP_FROMALTSTACK, Opcodes::OP_GREATERTHAN, Opcodes::OP_GREATERTHANOREQUAL, Opcodes::OP_HASH160, Opcodes::OP_HASH256, Opcodes::OP_IF, Opcodes::OP_IFDUP, Opcodes::OP_INVALIDOPCODE, Opcodes::OP_INVERT, Opcodes::OP_LEFT, Opcodes::OP_LESSTHAN, Opcodes::OP_LESSTHANOREQUAL, Opcodes::OP_LSHIFT, Opcodes::OP_MAX, Opcodes::OP_MIN, Opcodes::OP_MOD, Opcodes::OP_MUL, Opcodes::OP_NEGATE, Opcodes::OP_NIP, Opcodes::OP_NOP, Opcodes::OP_NOP1, Opcodes::OP_NOP10, Opcodes::OP_NOP2, Opcodes::OP_NOP3, Opcodes::OP_NOP4, Opcodes::OP_NOP5, Opcodes::OP_NOP6, Opcodes::OP_NOP7, Opcodes::OP_NOP8, Opcodes::OP_NOP9, Opcodes::OP_NOT, Opcodes::OP_NOTIF, Opcodes::OP_NUMEQUAL, Opcodes::OP_NUMEQUALVERIFY, Opcodes::OP_NUMNOTEQUAL, Opcodes::OP_OR, Opcodes::OP_OVER, Opcodes::OP_PICK, Opcodes::OP_PUBKEY, Opcodes::OP_PUBKEYHASH, Opcodes::OP_PUSHDATA1, Opcodes::OP_PUSHDATA2, Opcodes::OP_PUSHDATA4, Opcodes::OP_RESERVED, Opcodes::OP_RESERVED1, Opcodes::OP_RESERVED2, Opcodes::OP_RETURN, Opcodes::OP_RIGHT, Opcodes::OP_RIPEMD160, Opcodes::OP_ROLL, Opcodes::OP_ROT, Opcodes::OP_RSHIFT, Opcodes::OP_SHA1, Opcodes::OP_SHA256, Opcodes::OP_SIZE, Opcodes::OP_SUB, Opcodes::OP_SUBSTR, Opcodes::OP_SWAP, Opcodes::OP_TOALTSTACK, Opcodes::OP_TUCK, Opcodes::OP_VER, Opcodes::OP_VERIF, Opcodes::OP_VERIFY, Opcodes::OP_VERNOTIF, Opcodes::OP_WITHIN, Opcodes::OP_XOR

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Opcodes

defined?, name_to_opcode, opcode_to_name, opcode_to_small_int, small_int_to_opcode

Constructor Details

#initializeScript

Returns a new instance of Script.



12
13
14
# File 'lib/bitcoin/script/script.rb', line 12

def initialize
  @chunks = []
end

Instance Attribute Details

#chunksObject

Returns the value of attribute chunks.



10
11
12
# File 'lib/bitcoin/script/script.rb', line 10

def chunks
  @chunks
end

Class Method Details

.decode_number(s) ⇒ Object

decode script number hex to int value



376
377
378
379
380
381
382
383
384
# File 'lib/bitcoin/script/script.rb', line 376

def self.decode_number(s)
  v = s.htb.reverse
  return 0 if v.length.zero?
  mbs = v[0].unpack('C').first
  v[0] = [mbs - 0x80].pack('C') unless (mbs & 0x80) == 0
  result = v.bth.to_i(16)
  result = -result unless (mbs & 0x80) == 0
  result
end

.encode_number(i) ⇒ Object

encode int value to script number hex. The stacks hold byte vectors. When used as numbers, byte vectors are interpreted as little-endian variable-length integers with the most significant bit determining the sign of the integer. Thus 0x81 represents -1. 0x80 is another representation of zero (so called negative 0). Positive 0 is represented by a null-length vector. Byte vectors are interpreted as Booleans where False is represented by any representation of zero, and True is represented by any representation of non-zero.



362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/bitcoin/script/script.rb', line 362

def self.encode_number(i)
  return '' if i == 0
  negative = i < 0

  hex = i.abs.to_even_length_hex
  hex = '0' + hex unless (hex.length % 2).zero?
  v = hex.htb.reverse # change endian

  v = v << (negative ? 0x80 : 0x00) unless (v[-1].unpack('C').first & 0x80) == 0
  v[-1] = [v[-1].unpack('C').first | 0x80].pack('C') if negative
  v.bth
end

.from_string(string) ⇒ Object

generate script from string.



70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/bitcoin/script/script.rb', line 70

def self.from_string(string)
  script = new
  string.split(' ').each do |v|
    opcode = Opcodes.name_to_opcode(v)
    if opcode
      script << (v =~ /^\d/ && Opcodes.small_int_to_opcode(v.ord) ? v.ord : opcode)
    else
      script << (v =~ /^[0-9]+$/ ? v.to_i : v)
    end
  end
  script
end

.pack_pushdata(data) ⇒ Object

binary data convert pushdata which contains data length and append PUSHDATA opcode if necessary.



387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/bitcoin/script/script.rb', line 387

def self.pack_pushdata(data)
  size = data.bytesize
  header = if size < OP_PUSHDATA1
             [size].pack('C')
           elsif size < 0xff
             [OP_PUSHDATA1, size].pack('CC')
           elsif size < 0xffff
             [OP_PUSHDATA2, size].pack('Cv')
           elsif size < 0xffffffff
             [OP_PUSHDATA4, size].pack('CV')
           else
             raise ArgumentError, 'data size is too big.'
           end
  header + data
end

.parse_from_addr(addr) ⇒ Bitcoin::Script

generate script from addr.

Parameters:

Returns:



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/bitcoin/script/script.rb', line 86

def self.parse_from_addr(addr)
  begin
    segwit_addr = Bech32::SegwitAddr.new(addr)
    raise 'Invalid hrp.' unless Bitcoin.chain_params.bech32_hrp == segwit_addr.hrp
    Bitcoin::Script.parse_from_payload(segwit_addr.to_script_pubkey.htb)
  rescue Exception => e
    hex, addr_version = Bitcoin.decode_base58_address(addr)
    case addr_version
    when Bitcoin.chain_params.address_version
      Bitcoin::Script.to_p2pkh(hex)
    when Bitcoin.chain_params.p2sh_version
      Bitcoin::Script.to_p2sh(hex)
    else
      throw e
    end
  end
end

.parse_from_payload(payload) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/bitcoin/script/script.rb', line 104

def self.parse_from_payload(payload)
  s = new
  buf = StringIO.new(payload)
  until buf.eof?
    opcode = buf.read(1)
    if opcode.pushdata?
      pushcode = opcode.ord
      packed_size = nil
      len = case pushcode
              when OP_PUSHDATA1
                packed_size = buf.read(1)
                packed_size.unpack('C').first
              when OP_PUSHDATA2
                packed_size = buf.read(2)
                packed_size.unpack('v').first
              when OP_PUSHDATA4
                packed_size = buf.read(4)
                packed_size.unpack('V').first
              else
                pushcode if pushcode < OP_PUSHDATA1
            end
      if len
        s.chunks << [len].pack('C') if buf.eof?
        unless buf.eof?
          chunk = (packed_size ? (opcode + packed_size) : (opcode)) + buf.read(len)
          s.chunks << chunk
        end
      end
    else
      s << opcode.ord
    end
  end
  s
end

.to_multisig_script(m, pubkeys) ⇒ Script

generate m of n multisig script

Parameters:

  • m (String)

    the number of signatures required for multisig

  • pubkeys (Array)

    array of public keys that compose multisig

Returns:

  • (Script)

    multisig script.



57
58
59
# File 'lib/bitcoin/script/script.rb', line 57

def self.to_multisig_script(m, pubkeys)
  new << m << pubkeys << pubkeys.size << OP_CHECKMULTISIG
end

.to_p2pkh(pubkey_hash) ⇒ Object

generate P2PKH script



17
18
19
# File 'lib/bitcoin/script/script.rb', line 17

def self.to_p2pkh(pubkey_hash)
  new << OP_DUP << OP_HASH160 << pubkey_hash << OP_EQUALVERIFY << OP_CHECKSIG
end

.to_p2sh(script_hash) ⇒ Script

generate p2sh script.

Parameters:

  • script_hash (String)

    script hash for P2SH

Returns:



38
39
40
# File 'lib/bitcoin/script/script.rb', line 38

def self.to_p2sh(script_hash)
  Script.new << OP_HASH160 << script_hash << OP_EQUAL
end

.to_p2sh_multisig_script(m, pubkeys) ⇒ Script

generate m of n multisig p2sh script

Parameters:

  • m (String)

    the number of signatures required for multisig

  • pubkeys (Array)

    array of public keys that compose multisig

Returns:

  • (Script, Script)

    first element is p2sh script, second one is redeem script.



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

def self.to_p2sh_multisig_script(m, pubkeys)
  redeem_script = to_multisig_script(m, pubkeys)
  [redeem_script.to_p2sh, redeem_script]
end

.to_p2wpkh(pubkey_hash) ⇒ Object

generate P2WPKH script



22
23
24
# File 'lib/bitcoin/script/script.rb', line 22

def self.to_p2wpkh(pubkey_hash)
  new << WITNESS_VERSION << pubkey_hash
end

.to_p2wsh(redeem_script) ⇒ Object

generate p2wsh script for redeem_script

Parameters:

  • redeem_script (Script)

    target redeem script

  • p2wsh (Script)

    script



65
66
67
# File 'lib/bitcoin/script/script.rb', line 65

def self.to_p2wsh(redeem_script)
  new << WITNESS_VERSION << redeem_script.to_sha256
end

Instance Method Details

#<<(obj) ⇒ Object

append object to payload



263
264
265
266
267
268
269
270
271
272
# File 'lib/bitcoin/script/script.rb', line 263

def <<(obj)
  if obj.is_a?(Integer)
    push_int(obj)
  elsif obj.is_a?(String)
    append_data(obj.b)
  elsif obj.is_a?(Array)
    obj.each { |o| self.<< o}
    self
  end
end

#==(other) ⇒ Object



463
464
465
466
# File 'lib/bitcoin/script/script.rb', line 463

def ==(other)
  return false unless other
  chunks == other.chunks
end

#addressesObject



151
152
153
154
155
156
157
# File 'lib/bitcoin/script/script.rb', line 151

def addresses
  return [p2pkh_addr] if p2pkh?
  return [p2sh_addr] if p2sh?
  return [bech32_addr] if witness_program?
  return get_multisig_pubkeys.map{|pubkey| Bitcoin::Key.new(pubkey: pubkey.bth).to_p2pkh} if multisig?
  []
end

#append_data(data) ⇒ Script

append data to payload with pushdata opcode

Parameters:

  • data (String)

    append data. this data is not binary

Returns:



297
298
299
300
# File 'lib/bitcoin/script/script.rb', line 297

def append_data(data)
  chunks << Bitcoin::Script.pack_pushdata(data.htb)
  self
end

#append_opcode(opcode) ⇒ Script

append opcode to payload

Parameters:

  • opcode (Integer)

    append opcode which defined by Bitcoin::Opcodes

Returns:

Raises:

  • (ArgumentError)


287
288
289
290
291
292
# File 'lib/bitcoin/script/script.rb', line 287

def append_opcode(opcode)
  opcode = Opcodes.small_int_to_opcode(opcode) if -1 <= opcode && opcode <= 16
  raise ArgumentError, "specified invalid opcode #{opcode}." unless Opcodes.defined?(opcode)
  chunks << opcode.chr
  self
end

#delete_opcode(opcode) ⇒ Object

remove all occurences of opcode. Typically it’s OP_CODESEPARATOR.



445
446
447
448
# File 'lib/bitcoin/script/script.rb', line 445

def delete_opcode(opcode)
  @chunks = chunks.select{|chunk| chunk.ord != opcode}
  self
end

#empty?Boolean

Returns:

  • (Boolean)


147
148
149
# File 'lib/bitcoin/script/script.rb', line 147

def empty?
  chunks.size == 0
end

#find_and_delete(subscript) ⇒ Object

removes chunks matching subscript byte-for-byte and returns as a new object.

Raises:

  • (ArgumentError)


411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/bitcoin/script/script.rb', line 411

def find_and_delete(subscript)
  raise ArgumentError, 'subscript must be Bitcoin::Script' unless subscript.is_a?(Script)
  return self if subscript.chunks.empty?
  buf = []
  i = 0
  result = Script.new
  chunks.each do |chunk|
    sub_chunk = subscript.chunks[i]
    if chunk.start_with?(sub_chunk)
      if chunk == sub_chunk
        buf << chunk
        i += 1
        (i = 0; buf.clear) if i == subscript.chunks.size # matched the whole subscript
      else # matched the part of head
        i = 0
        tmp = chunk.dup
        tmp.slice!(sub_chunk)
        result.chunks << tmp
      end
    else
      result.chunks << buf.join unless buf.empty?
      if buf.first == chunk
        i = 1
        buf = [chunk]
      else
        i = 0
        result.chunks << chunk
      end
    end
  end
  result
end

#get_multisig_pubkeysObject



48
49
50
51
# File 'lib/bitcoin/script/script.rb', line 48

def get_multisig_pubkeys
  num = Bitcoin::Opcodes.opcode_to_small_int(chunks[-2].bth.to_i(16))
  (1..num).map{ |i| chunks[i].pushed_data }
end

#include?(item) ⇒ Boolean

Check the item is in the chunk of the script.

Returns:

  • (Boolean)


303
304
305
306
307
308
309
310
311
312
# File 'lib/bitcoin/script/script.rb', line 303

def include?(item)
  chunk_item = if item.is_a?(Integer)
                 item.chr
               elsif item.is_a?(String)
                 data = Encoding::ASCII_8BIT == item.encoding ? item : item.htb
                 Bitcoin::Script.pack_pushdata(data)
               end
  return false unless chunk_item
  chunks.include?(chunk_item)
end

#multisig?Boolean

Returns:

  • (Boolean)


187
188
189
190
191
192
193
# File 'lib/bitcoin/script/script.rb', line 187

def multisig?
  return false if chunks.size < 4 || chunks.last.ord != OP_CHECKMULTISIG
  pubkey_count = Opcodes.opcode_to_small_int(chunks[-2].opcode)
  sig_count = Opcodes.opcode_to_small_int(chunks[0].opcode)
  return false unless pubkey_count || sig_count
  sig_count <= pubkey_count
end

#op_return?Boolean

Returns:

  • (Boolean)


195
196
197
# File 'lib/bitcoin/script/script.rb', line 195

def op_return?
  chunks.size >= 1 && chunks[0].ord == OP_RETURN
end

#op_return_dataObject



204
205
206
207
208
# File 'lib/bitcoin/script/script.rb', line 204

def op_return_data
  return nil unless op_return?
  return nil if chunks.size == 1
  chunks[1].pushed_data
end

#p2pkh?Boolean

whether this script is a P2PKH format script.

Returns:

  • (Boolean)


165
166
167
168
169
# File 'lib/bitcoin/script/script.rb', line 165

def p2pkh?
  return false unless chunks.size == 5
  [OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG] ==
      (chunks[0..1]+ chunks[3..4]).map(&:ord) && chunks[2].bytesize == 21
end

#p2sh?Boolean

Returns:

  • (Boolean)


182
183
184
185
# File 'lib/bitcoin/script/script.rb', line 182

def p2sh?
  return false unless chunks.size == 3
  OP_HASH160 == chunks[0].ord && OP_EQUAL == chunks[2].ord && chunks[1].bytesize == 21
end

#p2wpkh?Boolean

whether this script is a P2WPKH format script.

Returns:

  • (Boolean)


172
173
174
175
# File 'lib/bitcoin/script/script.rb', line 172

def p2wpkh?
  return false unless chunks.size == 2
  chunks[0].ord == WITNESS_VERSION && chunks[1].bytesize == 21
end

#p2wsh?Boolean

Returns:

  • (Boolean)


177
178
179
180
# File 'lib/bitcoin/script/script.rb', line 177

def p2wsh?
  return false unless chunks.size == 2
  chunks[0].ord == WITNESS_VERSION && chunks[1].bytesize == 33
end

#push_int(n) ⇒ Object

push integer to stack.



275
276
277
278
279
280
281
282
# File 'lib/bitcoin/script/script.rb', line 275

def push_int(n)
  begin
    append_opcode(n)
  rescue ArgumentError
    append_data(Script.encode_number(n))
  end
  self
end

#push_only?Boolean

whether data push only script which dose not include other opcode

Returns:

  • (Boolean)


211
212
213
214
215
216
# File 'lib/bitcoin/script/script.rb', line 211

def push_only?
  chunks.each do |c|
    return false if !c.opcode.nil? && c.opcode > OP_16
  end
  true
end

#sizeObject

script size



350
351
352
# File 'lib/bitcoin/script/script.rb', line 350

def size
  to_payload.bytesize
end

#standard?Boolean

check whether standard script.

Returns:

  • (Boolean)


160
161
162
# File 'lib/bitcoin/script/script.rb', line 160

def standard?
  p2pkh? | p2sh? | p2wpkh? | p2wsh? | multisig? | standard_op_return?
end

#standard_op_return?Boolean

Returns:

  • (Boolean)


199
200
201
202
# File 'lib/bitcoin/script/script.rb', line 199

def standard_op_return?
  op_return? && size <= MAX_OP_RETURN_RELAY &&
      (chunks.size == 1 || chunks[1].opcode <= OP_16)
end

#subscript(*args) ⇒ Object

subscript this script to the specified range.



404
405
406
407
408
# File 'lib/bitcoin/script/script.rb', line 404

def subscript(*args)
  s = self.class.new
  s.chunks = chunks[*args]
  s
end

#subscript_codeseparator(separator_index) ⇒ Object

Returns a script that deleted the script before the index specified by separator_index.



451
452
453
454
455
456
457
458
459
460
461
# File 'lib/bitcoin/script/script.rb', line 451

def subscript_codeseparator(separator_index)
  buf = []
  process_separator_index = 0
  chunks.each{|chunk|
    buf << chunk if process_separator_index == separator_index
    if chunk.ord == OP_CODESEPARATOR && process_separator_index < separator_index
      process_separator_index += 1
    end
  }
  buf.join
end

#to_hObject



477
478
479
480
481
482
483
484
485
# File 'lib/bitcoin/script/script.rb', line 477

def to_h
  h = {asm: to_s, hex: to_payload.bth, type: type}
  addrs = addresses
  unless addrs.empty?
    h[:req_sigs] = multisig? ? Bitcoin::Opcodes.opcode_to_small_int(chunks[0].bth.to_i(16)) :addrs.size
    h[:addresses] = addrs
  end
  h
end

#to_hash160Object

generate hash160 hash for payload



345
346
347
# File 'lib/bitcoin/script/script.rb', line 345

def to_hash160
  Bitcoin.hash160(to_payload.bth)
end

#to_hexObject



143
144
145
# File 'lib/bitcoin/script/script.rb', line 143

def to_hex
  to_payload.bth
end

#to_p2shScript

generate p2sh script with this as a redeem script

Returns:



44
45
46
# File 'lib/bitcoin/script/script.rb', line 44

def to_p2sh
  Script.to_p2sh(to_hash160)
end

#to_payloadObject



139
140
141
# File 'lib/bitcoin/script/script.rb', line 139

def to_payload
  chunks.join
end

#to_sObject



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/bitcoin/script/script.rb', line 314

def to_s
  chunks.map { |c|
    case c
    when Integer
      opcode_to_name(c)
    when String
      if c.pushdata?
        v = Opcodes.opcode_to_small_int(c.ord)
        if v
          v
        else
          data = c.pushed_data
          if data.bytesize <= 4
            Script.decode_number(data.bth) # for scriptnum
          else
            data.bth
          end
        end
      else
        Opcodes.opcode_to_name(c.ord)
      end
    end
  }.join(' ')
end

#to_script_code(skip_separator_index = 0) ⇒ Object

If this script is witness program, return its script code, otherwise returns the self payload. ScriptInterpreter does not use this.



245
246
247
248
249
250
251
252
253
# File 'lib/bitcoin/script/script.rb', line 245

def to_script_code(skip_separator_index = 0)
  payload = to_payload
  if p2wpkh?
    payload = Script.to_p2pkh(chunks[1].pushed_data.bth).to_payload
  elsif skip_separator_index > 0
    payload = subscript_codeseparator(skip_separator_index)
  end
  Bitcoin.pack_var_string(payload)
end

#to_sha256Object

generate sha-256 hash for payload



340
341
342
# File 'lib/bitcoin/script/script.rb', line 340

def to_sha256
  Bitcoin.sha256(to_payload).bth
end

#typeObject



468
469
470
471
472
473
474
475
# File 'lib/bitcoin/script/script.rb', line 468

def type
  return 'pubkeyhash' if p2pkh?
  return 'scripthash' if p2sh?
  return 'multisig' if multisig?
  return 'witness_v0_keyhash' if p2wpkh?
  return 'witness_v0_scripthash' if p2wsh?
  'nonstandard'
end

#witness_commitmentObject

get witness commitment



236
237
238
239
240
241
# File 'lib/bitcoin/script/script.rb', line 236

def witness_commitment
  return nil if !op_return? || op_return_data.bytesize < 36
  buf = StringIO.new(op_return_data)
  return nil unless buf.read(4).bth == WITNESS_COMMITMENT_HEADER
  buf.read(32).bth
end

#witness_dataObject

get witness version and witness program



256
257
258
259
260
# File 'lib/bitcoin/script/script.rb', line 256

def witness_data
  version = opcode_to_small_int(chunks[0].opcode)
  program = chunks[1].pushed_data
  [version, program]
end

#witness_program?Boolean

A witness program is any valid Script that consists of a 1-byte push opcode followed by a data push between 2 and 40 bytes.

Returns:

  • (Boolean)


219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/bitcoin/script/script.rb', line 219

def witness_program?
  return false if size < 4 || size > 42 || chunks.size < 2

  opcode = chunks[0].opcode

  return false if opcode != OP_0 && (opcode < OP_1 || opcode > OP_16)
  return false unless chunks[1].pushdata?

  if size == (chunks[1][0].unpack('C').first + 2)
    program_size = chunks[1].pushed_data.bytesize
    return program_size >= 2 && program_size <= 40
  end

  false
end