bitcoinrb 0.5.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +37 -0
  3. data/.rspec_parallel +2 -0
  4. data/.ruby-version +1 -1
  5. data/README.md +11 -1
  6. data/bitcoinrb.gemspec +7 -6
  7. data/lib/bitcoin/block_filter.rb +14 -0
  8. data/lib/bitcoin/chain_params.rb +9 -0
  9. data/lib/bitcoin/chainparams/signet.yml +39 -0
  10. data/lib/bitcoin/constants.rb +45 -4
  11. data/lib/bitcoin/descriptor.rb +1 -1
  12. data/lib/bitcoin/errors.rb +19 -0
  13. data/lib/bitcoin/ext/array_ext.rb +22 -0
  14. data/lib/bitcoin/ext/ecdsa.rb +36 -0
  15. data/lib/bitcoin/ext.rb +1 -0
  16. data/lib/bitcoin/ext_key.rb +36 -20
  17. data/lib/bitcoin/key.rb +85 -28
  18. data/lib/bitcoin/message/addr_v2.rb +34 -0
  19. data/lib/bitcoin/message/base.rb +16 -0
  20. data/lib/bitcoin/message/cfcheckpt.rb +2 -2
  21. data/lib/bitcoin/message/cfheaders.rb +1 -1
  22. data/lib/bitcoin/message/cfilter.rb +1 -1
  23. data/lib/bitcoin/message/fee_filter.rb +1 -1
  24. data/lib/bitcoin/message/filter_load.rb +3 -3
  25. data/lib/bitcoin/message/header_and_short_ids.rb +1 -1
  26. data/lib/bitcoin/message/inventory.rb +1 -1
  27. data/lib/bitcoin/message/merkle_block.rb +1 -1
  28. data/lib/bitcoin/message/network_addr.rb +141 -18
  29. data/lib/bitcoin/message/ping.rb +1 -1
  30. data/lib/bitcoin/message/pong.rb +1 -1
  31. data/lib/bitcoin/message/send_addr_v2.rb +13 -0
  32. data/lib/bitcoin/message/send_cmpct.rb +2 -2
  33. data/lib/bitcoin/message/tx.rb +1 -1
  34. data/lib/bitcoin/message.rb +72 -0
  35. data/lib/bitcoin/message_sign.rb +47 -0
  36. data/lib/bitcoin/mnemonic.rb +2 -2
  37. data/lib/bitcoin/network/peer_discovery.rb +1 -3
  38. data/lib/bitcoin/node/configuration.rb +3 -1
  39. data/lib/bitcoin/node/spv.rb +8 -0
  40. data/lib/bitcoin/opcodes.rb +14 -1
  41. data/lib/bitcoin/payment_code.rb +2 -2
  42. data/lib/bitcoin/payments/payment.pb.rb +1 -1
  43. data/lib/bitcoin/psbt/hd_key_path.rb +1 -1
  44. data/lib/bitcoin/psbt/input.rb +4 -4
  45. data/lib/bitcoin/psbt/output.rb +1 -1
  46. data/lib/bitcoin/psbt/tx.rb +14 -5
  47. data/lib/bitcoin/psbt.rb +8 -0
  48. data/lib/bitcoin/rpc/bitcoin_core_client.rb +1 -1
  49. data/lib/bitcoin/rpc/request_handler.rb +3 -3
  50. data/lib/bitcoin/script/script.rb +80 -30
  51. data/lib/bitcoin/script/script_error.rb +27 -1
  52. data/lib/bitcoin/script/script_interpreter.rb +164 -62
  53. data/lib/bitcoin/script/tx_checker.rb +62 -14
  54. data/lib/bitcoin/secp256k1/native.rb +184 -17
  55. data/lib/bitcoin/secp256k1/ruby.rb +108 -21
  56. data/lib/bitcoin/sighash_generator.rb +157 -0
  57. data/lib/bitcoin/taproot/leaf_node.rb +23 -0
  58. data/lib/bitcoin/taproot/simple_builder.rb +155 -0
  59. data/lib/bitcoin/taproot.rb +45 -0
  60. data/lib/bitcoin/tx.rb +30 -96
  61. data/lib/bitcoin/tx_in.rb +1 -1
  62. data/lib/bitcoin/tx_out.rb +2 -3
  63. data/lib/bitcoin/util.rb +15 -6
  64. data/lib/bitcoin/version.rb +1 -1
  65. data/lib/bitcoin/wallet/account.rb +1 -1
  66. data/lib/bitcoin.rb +32 -24
  67. metadata +58 -18
  68. data/.travis.yml +0 -12
@@ -1,6 +1,8 @@
1
1
  module Bitcoin
2
2
  module Message
3
3
 
4
+ class Error < StandardError; end
5
+
4
6
  autoload :Base, 'bitcoin/message/base'
5
7
  autoload :Inventory, 'bitcoin/message/inventory'
6
8
  autoload :InventoriesParser, 'bitcoin/message/inventories_parser'
@@ -44,6 +46,8 @@ module Bitcoin
44
46
  autoload :CFCheckpt, 'bitcoin/message/cfcheckpt'
45
47
  autoload :CFilter, 'bitcoin/message/cfilter'
46
48
  autoload :CFHeaders, 'bitcoin/message/cfheaders'
49
+ autoload :SendAddrV2, 'bitcoin/message/send_addr_v2'
50
+ autoload :AddrV2, 'bitcoin/message/addr_v2'
47
51
 
48
52
  USER_AGENT = "/bitcoinrb:#{Bitcoin::VERSION}/"
49
53
 
@@ -73,5 +77,73 @@ module Bitcoin
73
77
  compact_witness: 70015
74
78
  }
75
79
 
80
+ module_function
81
+
82
+ # Decode P2P message.
83
+ # @param [String] command P2P message command string.
84
+ # @param [String] payload P2P message payload with hex format..
85
+ # @return [Bitcoin::Message::]
86
+ def decode(command, payload = nil)
87
+ payload = payload.htb if payload
88
+ case command
89
+ when Bitcoin::Message::Version::COMMAND
90
+ Bitcoin::Message::Version.parse_from_payload(payload)
91
+ when Bitcoin::Message::VerAck::COMMAND
92
+ Bitcoin::Message::VerAck.new
93
+ when Bitcoin::Message::GetAddr::COMMAND
94
+ Bitcoin::Message::GetAddr.new
95
+ when Bitcoin::Message::Addr::COMMAND
96
+ Bitcoin::Message::Addr.parse_from_payload(payload)
97
+ when Bitcoin::Message::SendHeaders::COMMAND
98
+ Bitcoin::Message::SendHeaders.new
99
+ when Bitcoin::Message::FeeFilter::COMMAND
100
+ Bitcoin::Message::FeeFilter.parse_from_payload(payload)
101
+ when Bitcoin::Message::Ping::COMMAND
102
+ Bitcoin::Message::Ping.parse_from_payload(payload)
103
+ when Bitcoin::Message::Pong::COMMAND
104
+ Bitcoin::Message::Pong.parse_from_payload(payload)
105
+ when Bitcoin::Message::GetHeaders::COMMAND
106
+ Bitcoin::Message::GetHeaders.parse_from_payload(payload)
107
+ when Bitcoin::Message::Headers::COMMAND
108
+ Bitcoin::Message::Headers.parse_from_payload(payload)
109
+ when Bitcoin::Message::Block::COMMAND
110
+ Bitcoin::Message::Block.parse_from_payload(payload)
111
+ when Bitcoin::Message::Tx::COMMAND
112
+ Bitcoin::Message::Tx.parse_from_payload(payload)
113
+ when Bitcoin::Message::NotFound::COMMAND
114
+ Bitcoin::Message::NotFound.parse_from_payload(payload)
115
+ when Bitcoin::Message::MemPool::COMMAND
116
+ Bitcoin::Message::MemPool.new
117
+ when Bitcoin::Message::Reject::COMMAND
118
+ Bitcoin::Message::Reject.parse_from_payload(payload)
119
+ when Bitcoin::Message::SendCmpct::COMMAND
120
+ Bitcoin::Message::SendCmpct.parse_from_payload(payload)
121
+ when Bitcoin::Message::Inv::COMMAND
122
+ Bitcoin::Message::Inv.parse_from_payload(payload)
123
+ when Bitcoin::Message::MerkleBlock::COMMAND
124
+ Bitcoin::Message::MerkleBlock.parse_from_payload(payload)
125
+ when Bitcoin::Message::CmpctBlock::COMMAND
126
+ Bitcoin::Message::CmpctBlock.parse_from_payload(payload)
127
+ when Bitcoin::Message::GetData::COMMAND
128
+ Bitcoin::Message::GetData.parse_from_payload(payload)
129
+ when Bitcoin::Message::GetCFHeaders::COMMAND
130
+ Bitcoin::Message::GetCFHeaders.parse_from_payload(payload)
131
+ when Bitcoin::Message::GetCFilters::COMMAND
132
+ Bitcoin::Message::GetCFilters.parse_from_payload(payload)
133
+ when Bitcoin::Message::GetCFCheckpt::COMMAND
134
+ Bitcoin::Message::GetCFCheckpt.parse_from_payload(payload)
135
+ when Bitcoin::Message::CFCheckpt::COMMAND
136
+ Bitcoin::Message::CFCheckpt.parse_from_payload(payload)
137
+ when Bitcoin::Message::CFHeaders::COMMAND
138
+ Bitcoin::Message::CFHeaders.parse_from_payload(payload)
139
+ when Bitcoin::Message::CFilter::COMMAND
140
+ Bitcoin::Message::CFilter.parse_from_payload(payload)
141
+ when Bitcoin::Message::SendAddrV2::COMMAND
142
+ Bitcoin::Message::SendAddrV2.new
143
+ when Bitcoin::Message::AddrV2::COMMAND
144
+ Bitcoin::Message::AddrV2.parse_from_payload(payload)
145
+ end
146
+ end
147
+
76
148
  end
77
149
  end
@@ -0,0 +1,47 @@
1
+ module Bitcoin
2
+
3
+ module MessageSign
4
+
5
+ class Error < StandardError; end
6
+
7
+ module_function
8
+
9
+ # Sign a message.
10
+ # @param [Bitcoin::Key] key Private key to sign with.
11
+ # @param [String] message The message to sign.
12
+ # @return [String] Signature, base64 encoded.
13
+ def sign_message(key, message, prefix: Bitcoin.chain_params.message_magic)
14
+ digest = message_hash(message, prefix: prefix)
15
+ compact_sig = key.sign_compact(digest)
16
+ Base64.strict_encode64(compact_sig)
17
+ end
18
+
19
+ # Verify a signed message.
20
+ # @param [String] address Signer's bitcoin address, it must refer to a public key.
21
+ # @param [String] signature The signature in base64 format.
22
+ # @param [String] message The message that was signed.
23
+ # @return [Boolean] Verification result.
24
+ def verify_message(address, signature, message, prefix: Bitcoin.chain_params.message_magic)
25
+ validate_address!(address)
26
+ sig = Base64.decode64(signature)
27
+ raise ArgumentError, 'Invalid signature length' unless sig.bytesize == Bitcoin::Key::COMPACT_SIGNATURE_SIZE
28
+ digest = message_hash(message, prefix: prefix)
29
+ pubkey = Bitcoin::Key.recover_compact(digest, sig)
30
+ return false unless pubkey
31
+ pubkey.to_p2pkh == address
32
+ end
33
+
34
+ # Hashes a message for signing and verification.
35
+ def message_hash(message, prefix: Bitcoin.chain_params.message_magic)
36
+ Bitcoin.double_sha256(Bitcoin.pack_var_string(prefix) << Bitcoin.pack_var_string(message))
37
+ end
38
+
39
+ def validate_address!(address)
40
+ raise ArgumentError, 'Invalid address' unless Bitcoin.valid_address?(address)
41
+ script = Bitcoin::Script.parse_from_addr(address)
42
+ raise ArgumentError, 'Address has no key' unless script.p2pkh?
43
+ end
44
+
45
+ private_class_method :validate_address!
46
+ end
47
+ end
@@ -39,7 +39,7 @@ module Bitcoin
39
39
  # @return [Array] the array of mnemonic word.
40
40
  def to_mnemonic(entropy)
41
41
  raise ArgumentError, 'entropy is empty.' if entropy.nil? || entropy.empty?
42
- e = entropy.htb.unpack('B*').first
42
+ e = entropy.htb.unpack1('B*')
43
43
  seed = e + checksum(e)
44
44
  mnemonic_index = seed.chars.each_slice(11).map{|i|i.join.to_i(2)}
45
45
  word_master = load_words
@@ -61,7 +61,7 @@ module Bitcoin
61
61
  # @param [String] entropy an entropy with bit string format
62
62
  # @return [String] an entropy checksum with bit string format
63
63
  def checksum(entropy)
64
- b = Bitcoin.sha256([entropy].pack('B*')).unpack('B*').first
64
+ b = Bitcoin.sha256([entropy].pack('B*')).unpack1('B*')
65
65
  b.slice(0, (entropy.length/32))
66
66
  end
67
67
 
@@ -30,9 +30,7 @@ module Bitcoin
30
30
  logger.debug 'discover peer address from DNS seeds.'
31
31
  dns_seeds.map { |seed|
32
32
  begin
33
- # x5 is a prefix for finding nodes that support NODE_BLOOM.
34
- # https://github.com/bitcoin/bitcoin/pull/8083#issuecomment-221552835
35
- Socket.getaddrinfo("x5.#{seed}", Bitcoin.chain_params.default_port).map{|a|a[2]}.uniq
33
+ Socket.getaddrinfo("#{seed}", Bitcoin.chain_params.default_port).map{|a|a[2]}.uniq
36
34
  rescue SocketError => e
37
35
  logger.error "SocketError occurred when load DNS seed: #{seed}, error: #{e.message}"
38
36
  nil
@@ -4,8 +4,10 @@ module Bitcoin
4
4
  module Node
5
5
  class Configuration
6
6
 
7
- attr_reader :conf
7
+ attr_reader :conf # Hash
8
8
 
9
+ # initialize configuration
10
+ # @param [Hash] opts parameter for node.
9
11
  def initialize(opts = {})
10
12
  # TODO apply configuration file.
11
13
  opts[:network] = :mainnet unless opts[:network]
@@ -13,6 +13,14 @@ module Bitcoin
13
13
  attr_accessor :wallet
14
14
  attr_accessor :bloom
15
15
 
16
+ # Initialize spv settings
17
+ # @param [Bitcoin::Node::Configuration] configuration configuration for spv.
18
+ #
19
+ # ```ruby
20
+ # config = Bitcoin::Node::Configuration.new(network: :mainnet)
21
+ # spv = Bitcoin::Node::SPV.new(config)
22
+ # spv.run
23
+ # ````
16
24
  def initialize(configuration)
17
25
  @chain = Bitcoin::Store::SPVChain.new
18
26
  @configuration = configuration
@@ -136,6 +136,8 @@ module Bitcoin
136
136
  OP_NOP9 = 0xb8
137
137
  OP_NOP10 = 0xb9
138
138
 
139
+ OP_CHECKSIGADD = 0xba # BIP 342 opcodes (Tapscript)
140
+
139
141
  # https://en.bitcoin.it/wiki/Script#Pseudo-words
140
142
  OP_PUBKEYHASH = 0xfd
141
143
  OP_PUBKEY = 0xfe
@@ -145,6 +147,9 @@ module Bitcoin
145
147
  OPCODES_MAP = Hash[*(constants.grep(/^OP_/) - [:OP_NOP2, :OP_NOP3, :OP_CHECKLOCKTIMEVERIFY, :OP_CHECKSEQUENCEVERIFY]).map { |c| [const_get(c), c.to_s] }.flatten]
146
148
  NAME_MAP = Hash[*constants.grep(/^OP_/).map { |c| [c.to_s, const_get(c)] }.flatten]
147
149
 
150
+ OP_SUCCESSES = [0x50, 0x62, 0x89, 0x8a, 0x8d, 0x8e, (0x7e..0x81).to_a,
151
+ (0x83..0x86).to_a, (0x95..0x99).to_a, (0xbb..0xfe).to_a].flatten
152
+
148
153
  def opcode_to_name(opcode)
149
154
  return OPCODES_MAP[opcode].delete('OP_') if opcode == OP_0 || (opcode <= OP_16 && opcode >= OP_1)
150
155
  OPCODES_MAP[opcode]
@@ -156,7 +161,8 @@ module Bitcoin
156
161
  end
157
162
 
158
163
  # whether opcode is predefined opcode
159
- def defined?(opcode)
164
+ def defined?(opcode, allow_success = false)
165
+ return true if allow_success && op_success?(opcode)
160
166
  !opcode_to_name(opcode).nil?
161
167
  end
162
168
 
@@ -174,5 +180,12 @@ module Bitcoin
174
180
  nil
175
181
  end
176
182
 
183
+ # Check whether +opcode+ is OP_SUCCESSx or not?
184
+ # @param [Integer] opcode an opcode.
185
+ # @return [Boolean] if +opcode+ is OP_SUCCESSx return true, otherwise false.
186
+ def op_success?(opcode)
187
+ OP_SUCCESSES.include?(opcode)
188
+ end
189
+
177
190
  end
178
191
  end
@@ -61,8 +61,8 @@ module Bitcoin
61
61
  raise ArgumentError, 'invalid version byte' unless hex[0..1] == VERSION_BYTE
62
62
  raise ArgumentError, 'invalid version' unless PaymentCode.support_version?(version)
63
63
  raise ArgumentError, 'invalid sign' unless PaymentCode.support_sign?(sign)
64
- raise ArgumentError, 'invalid public key' unless Bitcoin::Key.new(priv_key: nil, pubkey: sign + public_key).fully_valid_pubkey?
65
- raise ArgumentError, 'invalid checksum' unless Bitcoin.calc_checksum(payment_code) == hex[-8..-1]
64
+ raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless Bitcoin::Key.new(priv_key: nil, pubkey: sign + public_key).fully_valid_pubkey?
65
+ raise ArgumentError, Errors::Messages::INVALID_CHECKSUM unless Bitcoin.calc_checksum(payment_code) == hex[-8..-1]
66
66
 
67
67
  x_value = payment_code[8..71]
68
68
  chain_code_hex = payment_code[72..135]
@@ -17,7 +17,7 @@ module Bitcoin
17
17
  end
18
18
 
19
19
  def transactions
20
- @values[:transactions].map{|raw_tx|Bitcoin::Tx.parse_from_payload(raw_tx)}
20
+ @values[:transactions].map{|raw_tx|Bitcoin::Tx.parse_from_payload(raw_tx, strict: true)}
21
21
  end
22
22
 
23
23
  end
@@ -12,7 +12,7 @@ module Bitcoin
12
12
  pubkey = pubkey.encoding == Encoding::ASCII_8BIT ? pubkey : pubkey.htb
13
13
  raise ArgumentError, 'Size of key was not the expected size for the type BIP32 keypath.' unless [Bitcoin::Key::PUBLIC_KEY_SIZE, Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE].include?(pubkey.bytesize)
14
14
  pubkey = Bitcoin::Key.new(pubkey: pubkey.bth)
15
- raise ArgumentError, 'Invalid pubkey' unless pubkey.fully_valid_pubkey?
15
+ raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless pubkey.fully_valid_pubkey?
16
16
  @pubkey = pubkey.pubkey
17
17
  @info = info
18
18
  end
@@ -36,7 +36,7 @@ module Bitcoin
36
36
  found_sep = true
37
37
  break
38
38
  end
39
- key_type = buf.read(1).unpack('C').first
39
+ key_type = buf.read(1).unpack1('C')
40
40
  key = buf.read(key_len - 1)
41
41
  value = buf.read(Bitcoin.unpack_var_int_from_io(buf))
42
42
 
@@ -44,7 +44,7 @@ module Bitcoin
44
44
  when PSBT_IN_TYPES[:non_witness_utxo]
45
45
  raise ArgumentError, 'Invalid non-witness utxo typed key.' unless key_len == 1
46
46
  raise ArgumentError, 'Duplicate Key, input non-witness utxo already provided.' if input.non_witness_utxo
47
- input.non_witness_utxo = Bitcoin::Tx.parse_from_payload(value)
47
+ input.non_witness_utxo = Bitcoin::Tx.parse_from_payload(value, strict: true)
48
48
  when PSBT_IN_TYPES[:witness_utxo]
49
49
  raise ArgumentError, 'Invalid input witness utxo typed key.' unless key_len == 1
50
50
  raise ArgumentError, 'Duplicate Key, input witness utxo already provided.' if input.witness_utxo
@@ -54,13 +54,13 @@ module Bitcoin
54
54
  raise ArgumentError, 'Size of key was not the expected size for the type partial signature pubkey.'
55
55
  end
56
56
  pubkey = Bitcoin::Key.new(pubkey: key.bth)
57
- raise ArgumentError, 'Invalid pubkey.' unless pubkey.fully_valid_pubkey?
57
+ raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless pubkey.fully_valid_pubkey?
58
58
  raise ArgumentError, 'Duplicate Key, input partial signature for pubkey already provided.' if input.partial_sigs[pubkey.pubkey]
59
59
  input.partial_sigs[pubkey.pubkey] = value
60
60
  when PSBT_IN_TYPES[:sighash]
61
61
  raise ArgumentError, 'Invalid input sighash type typed key.' unless key_len == 1
62
62
  raise ArgumentError 'Duplicate Key, input sighash type already provided.' if input.sighash_type
63
- input.sighash_type = value.unpack('I').first
63
+ input.sighash_type = value.unpack1('I')
64
64
  when PSBT_IN_TYPES[:redeem_script]
65
65
  raise ArgumentError, 'Invalid redeemscript typed key.' unless key_len == 1
66
66
  raise ArgumentError, 'Duplicate Key, input redeemScript already provided.' if input.redeem_script
@@ -26,7 +26,7 @@ module Bitcoin
26
26
  found_sep = true
27
27
  break
28
28
  end
29
- key_type = buf.read(1).unpack('C').first
29
+ key_type = buf.read(1).unpack1('C')
30
30
  key = buf.read(key_len - 1)
31
31
  value = buf.read(Bitcoin.unpack_var_int_from_io(buf))
32
32
  case key_type
@@ -55,7 +55,7 @@ module Bitcoin
55
55
  # @return [Bitcoin::PartiallySignedTx]
56
56
  def self.parse_from_payload(payload)
57
57
  buf = StringIO.new(payload)
58
- raise ArgumentError, 'Invalid PSBT magic bytes.' unless buf.read(4).unpack('N').first == PSBT_MAGIC_BYTES
58
+ raise ArgumentError, 'Invalid PSBT magic bytes.' unless buf.read(4).unpack1('N') == PSBT_MAGIC_BYTES
59
59
  raise ArgumentError, 'Invalid PSBT separator.' unless buf.read(1).bth.to_i(16) == 0xff
60
60
  partial_tx = self.new
61
61
  found_sep = false
@@ -66,7 +66,7 @@ module Bitcoin
66
66
  found_sep = true
67
67
  break
68
68
  end
69
- key_type = buf.read(1).unpack('C').first
69
+ key_type = buf.read(1).unpack1('C')
70
70
  key = buf.read(key_len - 1)
71
71
  value = buf.read(Bitcoin.unpack_var_int_from_io(buf))
72
72
 
@@ -74,20 +74,20 @@ module Bitcoin
74
74
  when PSBT_GLOBAL_TYPES[:unsigned_tx]
75
75
  raise ArgumentError, 'Invalid global transaction typed key.' unless key_len == 1
76
76
  raise ArgumentError, 'Duplicate Key, unsigned tx already provided.' if partial_tx.tx
77
- partial_tx.tx = Bitcoin::Tx.parse_from_payload(value, non_witness: true)
77
+ partial_tx.tx = Bitcoin::Tx.parse_from_payload(value, non_witness: true, strict: true)
78
78
  partial_tx.tx.in.each do |tx_in|
79
79
  raise ArgumentError, 'Unsigned tx does not have empty scriptSigs and scriptWitnesses.' if !tx_in.script_sig.empty? || !tx_in.script_witness.empty?
80
80
  end
81
81
  when PSBT_GLOBAL_TYPES[:xpub]
82
82
  raise ArgumentError, 'Size of key was not the expected size for the type global xpub.' unless key.size == Bitcoin::BIP32_EXTKEY_WITH_VERSION_SIZE
83
83
  xpub = Bitcoin::ExtPubkey.parse_from_payload(key)
84
- raise ArgumentError, 'Invalid pubkey.' unless xpub.key.fully_valid_pubkey?
84
+ raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless xpub.key.fully_valid_pubkey?
85
85
  raise ArgumentError, 'Duplicate key, global xpub already provided' if partial_tx.xpubs.any?{|x|x.xpub == xpub}
86
86
  info = Bitcoin::PSBT::KeyOriginInfo.parse_from_payload(value)
87
87
  raise ArgumentError, "global xpub's depth and the number of indexes not matched." unless xpub.depth == info.key_paths.size
88
88
  partial_tx.xpubs << Bitcoin::PSBT::GlobalXpub.new(xpub, info)
89
89
  when PSBT_GLOBAL_TYPES[:ver]
90
- partial_tx.version_number = value.unpack('V').first
90
+ partial_tx.version_number = value.unpack1('V')
91
91
  raise ArgumentError, "An unsupported version was detected." if SUPPORT_VERSION < partial_tx.version_number
92
92
  else
93
93
  raise ArgumentError, 'Duplicate Key, key for unknown value already provided.' if partial_tx.unknowns[key]
@@ -162,6 +162,15 @@ module Bitcoin
162
162
  Base64.strict_encode64(to_payload)
163
163
  end
164
164
 
165
+ # Store the PSBT to a file.
166
+ # @param [String] path File path to store.
167
+ def to_file(path)
168
+ raise ArgumentError, 'The file already exists' if File.exist?(path)
169
+ File.open(path, 'w') do |f|
170
+ f.write(to_payload)
171
+ end
172
+ end
173
+
165
174
  # update input key-value maps.
166
175
  # @param [Bitcoin::Tx] prev_tx previous tx reference by input.
167
176
  # @param [Bitcoin::Script] redeem_script redeem script to set input.
data/lib/bitcoin/psbt.rb CHANGED
@@ -31,6 +31,14 @@ module Bitcoin
31
31
  s << Bitcoin.pack_var_int(value.bytesize) << value
32
32
  s
33
33
  end
34
+
35
+ # Load PSBT from file.
36
+ # @param [String] path File path of PSBT.
37
+ # @return [Bitcoin::PSBT::Tx] PSBT object.
38
+ def load_from_file(path)
39
+ raise ArgumentError, 'File not found' unless File.exist?(path)
40
+ Bitcoin::PSBT::Tx.parse_from_payload(File.read(path))
41
+ end
34
42
  end
35
43
 
36
44
  end
@@ -63,7 +63,7 @@ module Bitcoin
63
63
  response = http.request(request)
64
64
  body = response.body
65
65
  response = Bitcoin::Ext::JsonParser.new(body.gsub(/\\u([\da-fA-F]{4})/) { [$1].pack('H*').unpack('n*').pack('U*').encode('ISO-8859-1').force_encoding('UTF-8') }).parse
66
- raise response['error'].to_s if response['error']
66
+ raise response['error'].to_json if response['error']
67
67
  response['result']
68
68
  end
69
69
 
@@ -49,7 +49,7 @@ module Bitcoin
49
49
  # Returns connected peer information.
50
50
  def getpeerinfo
51
51
  node.pool.peers.map do |peer|
52
- local_addr = "#{peer.remote_version.remote_addr.ip}:18333"
52
+ local_addr = "#{peer.remote_version.remote_addr.addr_string}:18333"
53
53
  {
54
54
  id: peer.id,
55
55
  addr: "#{peer.host}:#{peer.port}",
@@ -75,7 +75,7 @@ module Bitcoin
75
75
 
76
76
  # broadcast transaction
77
77
  def sendrawtransaction(hex_tx)
78
- tx = Bitcoin::Tx.parse_from_payload(hex_tx.htb)
78
+ tx = Bitcoin::Tx.parse_from_payload(hex_tx.htb, strict: true)
79
79
  # TODO check wether tx is valid
80
80
  node.broadcast(tx)
81
81
  tx.txid
@@ -84,7 +84,7 @@ module Bitcoin
84
84
  # decode tx data.
85
85
  def decoderawtransaction(hex_tx)
86
86
  begin
87
- Bitcoin::Tx.parse_from_payload(hex_tx.htb).to_h
87
+ Bitcoin::Tx.parse_from_payload(hex_tx.htb, strict: true ).to_h
88
88
  rescue Exception
89
89
  raise ArgumentError.new('TX decode failed')
90
90
  end
@@ -21,7 +21,7 @@ module Bitcoin
21
21
 
22
22
  # generate P2WPKH script
23
23
  def self.to_p2wpkh(pubkey_hash)
24
- new << WITNESS_VERSION << pubkey_hash
24
+ new << WITNESS_VERSION_V0 << pubkey_hash
25
25
  end
26
26
 
27
27
  # generate m of n multisig p2sh script
@@ -52,7 +52,7 @@ module Bitcoin
52
52
  end
53
53
 
54
54
  # generate m of n multisig script
55
- # @param [String] m the number of signatures required for multisig
55
+ # @param [Integer] m the number of signatures required for multisig
56
56
  # @param [Array] pubkeys array of public keys that compose multisig
57
57
  # @return [Script] multisig script.
58
58
  def self.to_multisig_script(m, pubkeys, sort: false)
@@ -64,7 +64,7 @@ module Bitcoin
64
64
  # @param [Script] redeem_script target redeem script
65
65
  # @param [Script] p2wsh script
66
66
  def self.to_p2wsh(redeem_script)
67
- new << WITNESS_VERSION << redeem_script.to_sha256
67
+ new << WITNESS_VERSION_V0 << redeem_script.to_sha256
68
68
  end
69
69
 
70
70
  # generate script from string.
@@ -87,17 +87,21 @@ module Bitcoin
87
87
  def self.parse_from_addr(addr)
88
88
  begin
89
89
  segwit_addr = Bech32::SegwitAddr.new(addr)
90
- raise 'Invalid hrp.' unless Bitcoin.chain_params.bech32_hrp == segwit_addr.hrp
90
+ raise ArgumentError, 'Invalid address.' unless Bitcoin.chain_params.bech32_hrp == segwit_addr.hrp
91
91
  Bitcoin::Script.parse_from_payload(segwit_addr.to_script_pubkey.htb)
92
92
  rescue Exception => e
93
+ begin
93
94
  hex, addr_version = Bitcoin.decode_base58_address(addr)
95
+ rescue
96
+ raise ArgumentError, 'Invalid address.'
97
+ end
94
98
  case addr_version
95
99
  when Bitcoin.chain_params.address_version
96
100
  Bitcoin::Script.to_p2pkh(hex)
97
101
  when Bitcoin.chain_params.p2sh_version
98
102
  Bitcoin::Script.to_p2sh(hex)
99
103
  else
100
- throw e
104
+ raise ArgumentError, 'Invalid address.'
101
105
  end
102
106
  end
103
107
  end
@@ -110,25 +114,28 @@ module Bitcoin
110
114
  if opcode.pushdata?
111
115
  pushcode = opcode.ord
112
116
  packed_size = nil
117
+ if buf.eof?
118
+ s.chunks << opcode
119
+ return s
120
+ end
113
121
  len = case pushcode
114
122
  when OP_PUSHDATA1
115
123
  packed_size = buf.read(1)
116
- packed_size.unpack('C').first
124
+ packed_size.unpack1('C')
117
125
  when OP_PUSHDATA2
118
126
  packed_size = buf.read(2)
119
- packed_size.unpack('v').first
127
+ packed_size.unpack1('v')
120
128
  when OP_PUSHDATA4
121
129
  packed_size = buf.read(4)
122
- packed_size.unpack('V').first
130
+ packed_size.unpack1('V')
123
131
  else
124
- pushcode if pushcode < OP_PUSHDATA1
132
+ pushcode < OP_PUSHDATA1 ? pushcode : 0
125
133
  end
126
- if len
127
- s.chunks << [len].pack('C') if buf.eof?
128
- unless buf.eof?
129
- chunk = (packed_size ? (opcode + packed_size) : (opcode)) + buf.read(len)
130
- s.chunks << chunk
131
- end
134
+ if buf.eof?
135
+ s.chunks << [len].pack('C')
136
+ else buf.eof?
137
+ chunk = (packed_size ? (opcode + packed_size) : (opcode)) + buf.read(len)
138
+ s.chunks << chunk
132
139
  end
133
140
  else
134
141
  if Opcodes.defined?(opcode.ord)
@@ -141,8 +148,12 @@ module Bitcoin
141
148
  s
142
149
  end
143
150
 
144
- def to_payload
145
- chunks.join
151
+ # Output script payload.
152
+ # @param [Boolean] length_prefixed Flag whether the length of the payload should be given at the beginning.(default: false)
153
+ # @return [String] payload
154
+ def to_payload(length_prefixed = false)
155
+ p = chunks.join
156
+ length_prefixed ? (Bitcoin.pack_var_int(p.length) << p) : p
146
157
  end
147
158
 
148
159
  def empty?
@@ -170,27 +181,40 @@ module Bitcoin
170
181
 
171
182
  # check whether standard script.
172
183
  def standard?
173
- p2pkh? | p2sh? | p2wpkh? | p2wsh? | multisig? | standard_op_return?
184
+ p2pkh? | p2sh? | p2wpkh? | p2wsh? | p2tr? | multisig? | standard_op_return?
174
185
  end
175
186
 
176
- # whether this script is a P2PKH format script.
187
+ # Check whether this script is a P2PKH format script.
188
+ # @return [Boolean] if P2PKH return true, otherwise false
177
189
  def p2pkh?
178
190
  return false unless chunks.size == 5
179
191
  [OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG] ==
180
192
  (chunks[0..1]+ chunks[3..4]).map(&:ord) && chunks[2].bytesize == 21
181
193
  end
182
194
 
183
- # whether this script is a P2WPKH format script.
195
+ # Check whether this script is a P2WPKH format script.
196
+ # @return [Boolean] if P2WPKH return true, otherwise false
184
197
  def p2wpkh?
185
198
  return false unless chunks.size == 2
186
- chunks[0].ord == WITNESS_VERSION && chunks[1].bytesize == 21
199
+ chunks[0].ord == WITNESS_VERSION_V0 && chunks[1].bytesize == 21
187
200
  end
188
201
 
202
+ # Check whether this script is a P2WPSH format script.
203
+ # @return [Boolean] if P2WPSH return true, otherwise false
189
204
  def p2wsh?
190
205
  return false unless chunks.size == 2
191
- chunks[0].ord == WITNESS_VERSION && chunks[1].bytesize == 33
206
+ chunks[0].ord == WITNESS_VERSION_V0 && chunks[1].bytesize == 33
207
+ end
208
+
209
+ # Check whether this script is a P2TR format script.
210
+ # @return [Boolean] if P2TR return true, otherwise false
211
+ def p2tr?
212
+ return false unless chunks.size == 2
213
+ chunks[0].ord == WITNESS_VERSION_V1 && chunks[1].bytesize == 33
192
214
  end
193
215
 
216
+ # Check whether this script is a P2SH format script.
217
+ # @return [Boolean] if P2SH return true, otherwise false
194
218
  def p2sh?
195
219
  return false unless chunks.size == 3
196
220
  OP_HASH160 == chunks[0].ord && OP_EQUAL == chunks[2].ord && chunks[1].bytesize == 21
@@ -242,7 +266,7 @@ module Bitcoin
242
266
  return false if opcode != OP_0 && (opcode < OP_1 || opcode > OP_16)
243
267
  return false unless chunks[1].pushdata?
244
268
 
245
- if size == (chunks[1][0].unpack('C').first + 2)
269
+ if size == (chunks[1][0].unpack1('C') + 2)
246
270
  program_size = chunks[1].pushed_data.bytesize
247
271
  return program_size >= 2 && program_size <= 40
248
272
  end
@@ -343,10 +367,14 @@ module Bitcoin
343
367
  v
344
368
  else
345
369
  data = c.pushed_data
346
- if data.bytesize <= 4
347
- Script.decode_number(data.bth) # for scriptnum
370
+ if data
371
+ if data.bytesize <= 4
372
+ Script.decode_number(data.bth) # for scriptnum
373
+ else
374
+ data.bth
375
+ end
348
376
  else
349
- data.bth
377
+ c.bth
350
378
  end
351
379
  end
352
380
  else
@@ -393,8 +421,8 @@ module Bitcoin
393
421
  hex = '0' + hex unless (hex.length % 2).zero?
394
422
  v = hex.htb.reverse # change endian
395
423
 
396
- v = v << (negative ? 0x80 : 0x00) unless (v[-1].unpack('C').first & 0x80) == 0
397
- v[-1] = [v[-1].unpack('C').first | 0x80].pack('C') if negative
424
+ v = v << (negative ? 0x80 : 0x00) unless (v[-1].unpack1('C') & 0x80) == 0
425
+ v[-1] = [v[-1].unpack1('C') | 0x80].pack('C') if negative
398
426
  v.bth
399
427
  end
400
428
 
@@ -402,7 +430,7 @@ module Bitcoin
402
430
  def self.decode_number(s)
403
431
  v = s.htb.reverse
404
432
  return 0 if v.length.zero?
405
- mbs = v[0].unpack('C').first
433
+ mbs = v[0].unpack1('C')
406
434
  v[0] = [mbs - 0x80].pack('C') unless (mbs & 0x80) == 0
407
435
  result = v.bth.to_i(16)
408
436
  result = -result unless (mbs & 0x80) == 0
@@ -487,7 +515,7 @@ module Bitcoin
487
515
  end
488
516
 
489
517
  def ==(other)
490
- return false unless other
518
+ return false unless other.is_a?(Script)
491
519
  chunks == other.chunks
492
520
  end
493
521
 
@@ -553,6 +581,28 @@ module Bitcoin
553
581
  segwit_addr.addr
554
582
  end
555
583
 
584
+ # Check whether push data length is valid.
585
+ # @return [Boolean] if valid return true, otherwise false.
586
+ def valid_pushdata_length?(chunk)
587
+ buf = StringIO.new(chunk)
588
+ opcode = buf.read(1).ord
589
+ offset = 1
590
+ len = case opcode
591
+ when OP_PUSHDATA1
592
+ offset += 1
593
+ buf.read(1).unpack1('C')
594
+ when OP_PUSHDATA2
595
+ offset += 2
596
+ buf.read(2).unpack1('v')
597
+ when OP_PUSHDATA4
598
+ offset += 4
599
+ buf.read(4).unpack1('V')
600
+ else
601
+ opcode
602
+ end
603
+ chunk.bytesize == len + offset
604
+ end
605
+
556
606
  end
557
607
 
558
608
  end