class Bitcoin::Script
bitcoin script
Attributes
Public Class Methods
decode script number hex to int value
# File lib/bitcoin/script/script.rb, line 430 def self.decode_number(s) v = s.htb.reverse return 0 if v.length.zero? mbs = v[0].unpack1('C') 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 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.
# File lib/bitcoin/script/script.rb, line 416 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].unpack1('C') & 0x80) == 0 v[-1] = [v[-1].unpack1('C') | 0x80].pack('C') if negative v.bth end
generate script from string.
# File lib/bitcoin/script/script.rb, line 71 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
# File lib/bitcoin/script/script.rb, line 13 def initialize @chunks = [] end
binary data
convert pushdata which contains data length and append PUSHDATA opcode if necessary.
# File lib/bitcoin/script/script.rb, line 441 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
generate script from addr. @param [String] addr address. @return [Bitcoin::Script] parsed script.
# File lib/bitcoin/script/script.rb, line 87 def self.parse_from_addr(addr) begin segwit_addr = Bech32::SegwitAddr.new(addr) raise ArgumentError, 'Invalid address.' unless Bitcoin.chain_params.bech32_hrp == segwit_addr.hrp Bitcoin::Script.parse_from_payload(segwit_addr.to_script_pubkey.htb) rescue Exception => e begin hex, addr_version = Bitcoin.decode_base58_address(addr) rescue raise ArgumentError, 'Invalid address.' end 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 raise ArgumentError, 'Invalid address.' end end end
# File lib/bitcoin/script/script.rb, line 109 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 if buf.eof? s.chunks << opcode return s end len = case pushcode when OP_PUSHDATA1 packed_size = buf.read(1) packed_size.unpack1('C') when OP_PUSHDATA2 packed_size = buf.read(2) packed_size.unpack1('v') when OP_PUSHDATA4 packed_size = buf.read(4) packed_size.unpack1('V') else pushcode < OP_PUSHDATA1 ? pushcode : 0 end if buf.eof? s.chunks << [len].pack('C') else buf.eof? chunk = (packed_size ? (opcode + packed_size) : (opcode)) + buf.read(len) s.chunks << chunk end else if Opcodes.defined?(opcode.ord) s << opcode.ord else s.chunks << (opcode + buf.read) # If opcode is invalid, put all remaining data in last chunk. end end end s end
generate m of n multisig script @param [Integer] m the number of signatures required for multisig @param [Array] pubkeys array of public keys that compose multisig @return [Script] multisig script.
# File lib/bitcoin/script/script.rb, line 58 def self.to_multisig_script(m, pubkeys, sort: false) pubkeys = pubkeys.sort if sort new << m << pubkeys << pubkeys.size << OP_CHECKMULTISIG end
generate P2PKH script
# File lib/bitcoin/script/script.rb, line 18 def self.to_p2pkh(pubkey_hash) new << OP_DUP << OP_HASH160 << pubkey_hash << OP_EQUALVERIFY << OP_CHECKSIG end
generate p2sh script. @param [String] script_hash script hash for P2SH @return [Script] P2SH script
# File lib/bitcoin/script/script.rb, line 39 def self.to_p2sh(script_hash) Script.new << OP_HASH160 << script_hash << OP_EQUAL end
generate m of n multisig p2sh script @param [String] m the number of signatures required for multisig @param [Array] pubkeys array of public keys that compose multisig @return [Script, Script] first element is p2sh script, second one is redeem script.
# File lib/bitcoin/script/script.rb, line 31 def self.to_p2sh_multisig_script(m, pubkeys) redeem_script = to_multisig_script(m, pubkeys) [redeem_script.to_p2sh, redeem_script] end
generate P2WPKH script
# File lib/bitcoin/script/script.rb, line 23 def self.to_p2wpkh(pubkey_hash) new << WITNESS_VERSION_V0 << pubkey_hash end
generate p2wsh script for redeem_script
@param [Script] redeem_script target redeem script @param [Script] p2wsh script
# File lib/bitcoin/script/script.rb, line 66 def self.to_p2wsh(redeem_script) new << WITNESS_VERSION_V0 << redeem_script.to_sha256 end
Public Instance Methods
append object to payload
# File lib/bitcoin/script/script.rb, line 305 def <<(obj) if obj.is_a?(Integer) push_int(obj) elsif obj.is_a?(String) append_data(obj) elsif obj.is_a?(Array) obj.each { |o| self.<< o} self end end
# File lib/bitcoin/script/script.rb, line 517 def ==(other) return false unless other.is_a?(Script) chunks == other.chunks end
@deprecated
# File lib/bitcoin/script/script.rb, line 164 def addresses puts "WARNING: Bitcoin::Script#addresses is deprecated. Use Bitcoin::Script#to_addr instead." 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 to payload with pushdata opcode @param [String] data append data. this data is not binary @return [Script] return self
# File lib/bitcoin/script/script.rb, line 339 def append_data(data) data = Encoding::ASCII_8BIT == data.encoding ? data : data.htb chunks << Bitcoin::Script.pack_pushdata(data) self end
append opcode to payload @param [Integer] opcode append opcode which defined by Bitcoin::Opcodes
@return [Script] return self
# File lib/bitcoin/script/script.rb, line 329 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
remove all occurences of opcode. Typically it's OP_CODESEPARATOR.
# File lib/bitcoin/script/script.rb, line 499 def delete_opcode(opcode) @chunks = chunks.select{|chunk| chunk.ord != opcode} self end
# File lib/bitcoin/script/script.rb, line 159 def empty? chunks.size == 0 end
removes chunks matching subscript byte-for-byte and returns as a new object.
# File lib/bitcoin/script/script.rb, line 465 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
# File lib/bitcoin/script/script.rb, line 49 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
get public keys in the stack. @return[Array] an array of the pubkeys with hex format.
# File lib/bitcoin/script/script.rb, line 256 def get_pubkeys chunks.select{|c|c.pushdata? && [33, 65].include?(c.pushed_data.bytesize) && [2, 3, 4, 6, 7].include?(c.pushed_data[0].bth.to_i(16))}.map{|c|c.pushed_data.bth} end
Check the item is in the chunk of the script.
# File lib/bitcoin/script/script.rb, line 346 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
# File lib/bitcoin/script/script.rb, line 223 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
# File lib/bitcoin/script/script.rb, line 231 def op_return? chunks.size >= 1 && chunks[0].ord == OP_RETURN end
# File lib/bitcoin/script/script.rb, line 240 def op_return_data return nil unless op_return? return nil if chunks.size == 1 chunks[1].pushed_data end
Check whether this script is a P2PKH format script. @return [Boolean] if P2PKH return true, otherwise false
# File lib/bitcoin/script/script.rb, line 189 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
Check whether this script is a P2SH format script. @return [Boolean] if P2SH return true, otherwise false
# File lib/bitcoin/script/script.rb, line 218 def p2sh? return false unless chunks.size == 3 OP_HASH160 == chunks[0].ord && OP_EQUAL == chunks[2].ord && chunks[1].bytesize == 21 end
Check whether this script is a P2TR format script. @return [Boolean] if P2TR return true, otherwise false
# File lib/bitcoin/script/script.rb, line 211 def p2tr? return false unless chunks.size == 2 chunks[0].ord == WITNESS_VERSION_V1 && chunks[1].bytesize == 33 end
Check whether this script is a P2WPKH format script. @return [Boolean] if P2WPKH return true, otherwise false
# File lib/bitcoin/script/script.rb, line 197 def p2wpkh? return false unless chunks.size == 2 chunks[0].ord == WITNESS_VERSION_V0 && chunks[1].bytesize == 21 end
Check whether this script is a P2WPSH format script. @return [Boolean] if P2WPSH return true, otherwise false
# File lib/bitcoin/script/script.rb, line 204 def p2wsh? return false unless chunks.size == 2 chunks[0].ord == WITNESS_VERSION_V0 && chunks[1].bytesize == 33 end
push integer to stack.
# File lib/bitcoin/script/script.rb, line 317 def push_int(n) begin append_opcode(n) rescue ArgumentError append_data(Script.encode_number(n)) end self end
whether data push only script which dose not include other opcode
# File lib/bitcoin/script/script.rb, line 247 def push_only? chunks.each do |c| return false if !c.opcode.nil? && c.opcode > OP_16 end true end
execute script interpreter using this script for development.
# File lib/bitcoin/script/script.rb, line 404 def run Bitcoin::ScriptInterpreter.eval(Bitcoin::Script.new, self.dup) end
script size
# File lib/bitcoin/script/script.rb, line 399 def size to_payload.bytesize end
check whether standard script.
# File lib/bitcoin/script/script.rb, line 183 def standard? p2pkh? | p2sh? | p2wpkh? | p2wsh? | p2tr? | multisig? | standard_op_return? end
# File lib/bitcoin/script/script.rb, line 235 def standard_op_return? op_return? && size <= MAX_OP_RETURN_RELAY && (chunks.size == 1 || chunks[1].opcode <= OP_16) end
subscript this script to the specified range.
# File lib/bitcoin/script/script.rb, line 458 def subscript(*args) s = self.class.new s.chunks = chunks[*args] s end
Returns a script that deleted the script before the index specified by separator_index.
# File lib/bitcoin/script/script.rb, line 505 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
convert to address @return [String] if script type is p2pkh or p2sh or witness program, return address, otherwise nil.
# File lib/bitcoin/script/script.rb, line 175 def to_addr return p2pkh_addr if p2pkh? return p2sh_addr if p2sh? return bech32_addr if witness_program? nil end
# File lib/bitcoin/script/script.rb, line 531 def to_h h = {asm: to_s, hex: to_hex, 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
generate hash160 hash for payload
# File lib/bitcoin/script/script.rb, line 394 def to_hash160 Bitcoin.hash160(to_hex) end
generate p2sh script with this as a redeem script @return [Script] P2SH script
# File lib/bitcoin/script/script.rb, line 45 def to_p2sh Script.to_p2sh(to_hash160) end
Output script payload. @param [Boolean] length_prefixed Flag whether the length of the payload should be given at the beginning.(default: false) @return [String] payload
# File lib/bitcoin/script/script.rb, line 154 def to_payload(length_prefixed = false) p = chunks.join length_prefixed ? (Bitcoin.pack_var_int(p.length) << p) : p end
# File lib/bitcoin/script/script.rb, line 357 def to_s chunks.map { |c| case c when Integer opcode_to_name(c) when String return c if c.empty? if c.pushdata? v = Opcodes.opcode_to_small_int(c.ord) if v v else data = c.pushed_data if data if data.bytesize <= 4 Script.decode_number(data.bth) # for scriptnum else data.bth end else c.bth end end else opcode = Opcodes.opcode_to_name(c.ord) opcode ? opcode : 'OP_UNKNOWN [error]' end end }.join(' ') end
If this script is witness program, return its script code, otherwise returns the self payload. ScriptInterpreter
does not use this.
# File lib/bitcoin/script/script.rb, line 287 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
generate sha-256 hash for payload
# File lib/bitcoin/script/script.rb, line 389 def to_sha256 Bitcoin.sha256(to_payload).bth end
# File lib/bitcoin/script/script.rb, line 522 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
Returns whether the script is guaranteed to fail at execution, regardless of the initial stack. This allows outputs to be pruned instantly when entering the UTXO set. @return [Boolean] whether the script is guaranteed to fail at execution
# File lib/bitcoin/script/script.rb, line 544 def unspendable? (size > 0 && op_return?) || size > Bitcoin::MAX_SCRIPT_SIZE end
get witness commitment
# File lib/bitcoin/script/script.rb, line 278 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
get witness version and witness program
# File lib/bitcoin/script/script.rb, line 298 def witness_data version = opcode_to_small_int(chunks[0].opcode) program = chunks[1].pushed_data [version, program] end
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.
# File lib/bitcoin/script/script.rb, line 261 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].unpack1('C') + 2) program_size = chunks[1].pushed_data.bytesize return program_size >= 2 && program_size <= 40 end false end
Private Instance Methods
return bech32 address for payload
# File lib/bitcoin/script/script.rb, line 577 def bech32_addr segwit_addr = Bech32::SegwitAddr.new segwit_addr.hrp = Bitcoin.chain_params.bech32_hrp segwit_addr.script_pubkey = to_hex segwit_addr.addr end
generate p2pkh address. if script dose not p2pkh, return nil.
# File lib/bitcoin/script/script.rb, line 551 def p2pkh_addr return nil unless p2pkh? hash160 = chunks[2].pushed_data.bth return nil unless hash160.htb.bytesize == 20 Bitcoin.encode_base58_address(hash160, Bitcoin.chain_params.address_version) end
generate p2sh address. if script dose not p2sh, return nil.
# File lib/bitcoin/script/script.rb, line 564 def p2sh_addr return nil unless p2sh? hash160 = chunks[1].pushed_data.bth return nil unless hash160.htb.bytesize == 20 Bitcoin.encode_base58_address(hash160, Bitcoin.chain_params.p2sh_version) end
generate p2wpkh address. if script dose not p2wpkh, return nil.
# File lib/bitcoin/script/script.rb, line 559 def p2wpkh_addr p2wpkh? ? bech32_addr : nil end
generate p2wsh address. if script dose not p2wsh, return nil.
# File lib/bitcoin/script/script.rb, line 572 def p2wsh_addr p2wsh? ? bech32_addr : nil end
Check whether push data length is valid. @return [Boolean] if valid return true, otherwise false.
# File lib/bitcoin/script/script.rb, line 586 def valid_pushdata_length?(chunk) buf = StringIO.new(chunk) opcode = buf.read(1).ord offset = 1 len = case opcode when OP_PUSHDATA1 offset += 1 buf.read(1).unpack1('C') when OP_PUSHDATA2 offset += 2 buf.read(2).unpack1('v') when OP_PUSHDATA4 offset += 4 buf.read(4).unpack1('V') else opcode end chunk.bytesize == len + offset end