bitcoinrb 0.5.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
data/lib/bitcoin/key.rb CHANGED
@@ -10,6 +10,7 @@ module Bitcoin
10
10
  COMPRESSED_PUBLIC_KEY_SIZE = 33
11
11
  SIGNATURE_SIZE = 72
12
12
  COMPACT_SIGNATURE_SIZE = 65
13
+ COMPACT_SIG_HEADER_BYTE = 0x1b
13
14
 
14
15
  attr_accessor :priv_key
15
16
  attr_accessor :pubkey
@@ -28,8 +29,7 @@ module Bitcoin
28
29
  # @param [Integer] key_type a key type which determine address type.
29
30
  # @param [Boolean] compressed [Deprecated] whether public key is compressed.
30
31
  # @return [Bitcoin::Key] a key object.
31
- def initialize(priv_key: nil, pubkey: nil, key_type: nil, compressed: true)
32
- puts "[Warning] Use key_type parameter instead of compressed. compressed parameter removed in the future." if key_type.nil? && !compressed.nil? && pubkey.nil?
32
+ def initialize(priv_key: nil, pubkey: nil, key_type: nil, compressed: true, allow_hybrid: false)
33
33
  if key_type
34
34
  @key_type = key_type
35
35
  compressed = @key_type != TYPES[:uncompressed]
@@ -39,13 +39,14 @@ module Bitcoin
39
39
  @secp256k1_module = Bitcoin.secp_impl
40
40
  @priv_key = priv_key
41
41
  if @priv_key
42
- raise ArgumentError, 'private key is not on curve' unless validate_private_key_range(@priv_key)
42
+ raise ArgumentError, Errors::Messages::INVALID_PRIV_KEY unless validate_private_key_range(@priv_key)
43
43
  end
44
44
  if pubkey
45
45
  @pubkey = pubkey
46
46
  else
47
47
  @pubkey = generate_pubkey(priv_key, compressed: compressed) if priv_key
48
48
  end
49
+ raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless fully_valid_pubkey?(allow_hybrid)
49
50
  end
50
51
 
51
52
  # generate key pair
@@ -63,9 +64,9 @@ module Bitcoin
63
64
  data = hex[2...-8].htb
64
65
  checksum = hex[-8..-1]
65
66
  raise ArgumentError, 'invalid version' unless version == Bitcoin.chain_params.privkey_version
66
- raise ArgumentError, 'invalid checksum' unless Bitcoin.calc_checksum(version + data.bth) == checksum
67
+ raise ArgumentError, Errors::Messages::INVALID_CHECKSUM unless Bitcoin.calc_checksum(version + data.bth) == checksum
67
68
  key_len = data.bytesize
68
- if key_len == COMPRESSED_PUBLIC_KEY_SIZE && data[-1].unpack('C').first == 1
69
+ if key_len == COMPRESSED_PUBLIC_KEY_SIZE && data[-1].unpack1('C') == 1
69
70
  key_type = TYPES[:compressed]
70
71
  data = data[0..-2]
71
72
  elsif key_len == 32
@@ -76,6 +77,23 @@ module Bitcoin
76
77
  new(priv_key: data.bth, key_type: key_type)
77
78
  end
78
79
 
80
+ # Generate from xonly public key.
81
+ # @param [String] xonly_pubkey xonly public key with hex format.
82
+ # @return [Bitcoin::Key] key object has public key.
83
+ def self.from_xonly_pubkey(xonly_pubkey)
84
+ raise ArgumentError, 'xonly_pubkey must be 32 bytes' unless xonly_pubkey.htb.bytesize == 32
85
+ Bitcoin::Key.new(pubkey: "02#{xonly_pubkey}", key_type: TYPES[:compressed])
86
+ end
87
+
88
+ # Generate from public key point.
89
+ # @param [ECDSA::Point] point Public key point.
90
+ # @param [Boolean] compressed whether compressed or not.
91
+ # @return [Bitcoin::Key]
92
+ def self.from_point(point, compressed: true)
93
+ pubkey = ECDSA::Format::PointOctetString.encode(point, compression: compressed).bth
94
+ Bitcoin::Key.new(pubkey: pubkey, key_type: TYPES[:compressed])
95
+ end
96
+
79
97
  # export private key with wif format
80
98
  def to_wif
81
99
  version = Bitcoin.chain_params.privkey_version
@@ -88,30 +106,69 @@ module Bitcoin
88
106
  # sign +data+ with private key
89
107
  # @param [String] data a data to be signed with binary format
90
108
  # @param [Boolean] low_r flag to apply low-R.
91
- # @param [String] extra_entropy the extra entropy for rfc6979.
109
+ # @param [String] extra_entropy the extra entropy with binary format for rfc6979.
110
+ # @param [Symbol] algo signature algorithm. ecdsa(default) or schnorr.
92
111
  # @return [String] signature data with binary format
93
- def sign(data, low_r = true, extra_entropy = nil)
94
- sig = secp256k1_module.sign_data(data, priv_key, extra_entropy)
95
- if low_r && !sig_has_low_r?(sig)
96
- counter = 1
97
- until sig_has_low_r?(sig)
98
- extra_entropy = [counter].pack('I*').bth.ljust(64, '0').htb
99
- sig = secp256k1_module.sign_data(data, priv_key, extra_entropy)
100
- counter += 1
112
+ def sign(data, low_r = true, extra_entropy = nil, algo: :ecdsa)
113
+ case algo
114
+ when :ecdsa
115
+ sig = secp256k1_module.sign_data(data, priv_key, extra_entropy)
116
+ if low_r && !sig_has_low_r?(sig)
117
+ counter = 1
118
+ until sig_has_low_r?(sig)
119
+ extra_entropy = [counter].pack('I*').bth.ljust(64, '0').htb
120
+ sig = secp256k1_module.sign_data(data, priv_key, extra_entropy)
121
+ counter += 1
122
+ end
101
123
  end
124
+ sig
125
+ when :schnorr
126
+ secp256k1_module.sign_data(data, priv_key, extra_entropy, algo: :schnorr)
127
+ else
128
+ raise ArgumentError "Unsupported algo specified: #{algo}"
102
129
  end
103
- sig
130
+ end
131
+
132
+ # Sign compact signature.
133
+ # @param [String] data message digest to be signed.
134
+ # @return [String] compact signature with binary format.
135
+ def sign_compact(data)
136
+ signature, rec = secp256k1_module.sign_compact(data, priv_key)
137
+ rec = Bitcoin::Key::COMPACT_SIG_HEADER_BYTE + rec + (compressed? ? 4 : 0)
138
+ [rec].pack('C') + ECDSA::Format::IntegerOctetString.encode(signature.r, 32) +
139
+ ECDSA::Format::IntegerOctetString.encode(signature.s, 32)
140
+ end
141
+
142
+ # Recover public key from compact signature.
143
+ # @param [String] data message digest using signature.
144
+ # @param [String] signature signature with binary format.
145
+ # @return [Bitcoin::Key] Recovered public key.
146
+ def self.recover_compact(data, signature)
147
+ rec_id = signature.unpack1('C')
148
+ rec = rec_id - Bitcoin::Key::COMPACT_SIG_HEADER_BYTE
149
+ raise ArgumentError, 'Invalid signature parameter' if rec < 0 || rec > 15
150
+ rec = rec & 3
151
+ compressed = (rec_id - Bitcoin::Key::COMPACT_SIG_HEADER_BYTE) & 4 != 0
152
+ Bitcoin.secp_impl.recover_compact(data, signature, rec, compressed)
104
153
  end
105
154
 
106
155
  # verify signature using public key
107
156
  # @param [String] sig signature data with binary format
108
- # @param [String] origin original message
157
+ # @param [String] data original message
158
+ # @param [Symbol] algo signature algorithm. ecdsa(default) or schnorr.
109
159
  # @return [Boolean] verify result
110
- def verify(sig, origin)
160
+ def verify(sig, data, algo: :ecdsa)
111
161
  return false unless valid_pubkey?
112
162
  begin
113
- sig = ecdsa_signature_parse_der_lax(sig)
114
- secp256k1_module.verify_sig(origin, sig, pubkey)
163
+ case algo
164
+ when :ecdsa
165
+ sig = ecdsa_signature_parse_der_lax(sig)
166
+ secp256k1_module.verify_sig(data, sig, pubkey)
167
+ when :schnorr
168
+ secp256k1_module.verify_sig(data, sig, xonly_pubkey, algo: :schnorr)
169
+ else
170
+ false
171
+ end
115
172
  rescue Exception
116
173
  false
117
174
  end
@@ -148,10 +205,16 @@ module Bitcoin
148
205
  # @return [ECDSA::Point]
149
206
  def to_point
150
207
  p = pubkey
151
- p ||= generate_pubkey(priv_key, compressed: compressed)
208
+ p ||= generate_pubkey(priv_key, compressed: compressed?)
152
209
  ECDSA::Format::PointOctetString.decode(p.htb, Bitcoin::Secp256k1::GROUP)
153
210
  end
154
211
 
212
+ # get xonly public key (32 bytes).
213
+ # @return [String] xonly public key with hex format
214
+ def xonly_pubkey
215
+ pubkey[2..65]
216
+ end
217
+
155
218
  # check +pubkey+ (hex) is compress or uncompress pubkey.
156
219
  def self.compress_or_uncompress_pubkey?(pubkey)
157
220
  p = pubkey.htb
@@ -225,14 +288,8 @@ module Bitcoin
225
288
  end
226
289
 
227
290
  # fully validate whether this is a valid public key (more expensive than IsValid())
228
- def fully_valid_pubkey?
229
- return false unless valid_pubkey?
230
- begin
231
- point = ECDSA::Format::PointOctetString.decode(pubkey.htb, ECDSA::Group::Secp256k1)
232
- ECDSA::Group::Secp256k1.valid_public_key?(point)
233
- rescue ECDSA::Format::DecodeError
234
- false
235
- end
291
+ def fully_valid_pubkey?(allow_hybrid = false)
292
+ valid_pubkey? && secp256k1_module.parse_ec_pubkey?(pubkey, allow_hybrid)
236
293
  end
237
294
 
238
295
  private
@@ -0,0 +1,34 @@
1
+ module Bitcoin
2
+ module Message
3
+
4
+ # addrv2 message class.
5
+ # https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki
6
+ class AddrV2 < Base
7
+
8
+ COMMAND = 'addrv2'
9
+
10
+ attr_reader :addrs
11
+
12
+ def initialize(addrs = [])
13
+ @addrs = addrs
14
+ end
15
+
16
+ def self.parse_from_payload(payload)
17
+ buf = StringIO.new(payload)
18
+ addr_count = Bitcoin.unpack_var_int_from_io(buf)
19
+ v2 = new
20
+ addr_count.times do
21
+ v2.addrs << NetworkAddr.parse_from_payload(buf, type: NetworkAddr::TYPE[:addr_v2])
22
+ end
23
+ v2
24
+ end
25
+
26
+ def to_payload
27
+ buf = Bitcoin.pack_var_int(addrs.size)
28
+ buf << (addrs.map { |a| a.to_payload(type: NetworkAddr::TYPE[:addr_v2])}.join)
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+ end
@@ -23,6 +23,22 @@ module Bitcoin
23
23
  raise 'to_payload must be implemented in a child class.'
24
24
  end
25
25
 
26
+ # Decode message data to message object.
27
+ # @param [String] message with binary format.
28
+ # @return [Bitcoin::Message::XXX] An instance of a class that inherits Bitcoin::Message::Base
29
+ # @raise [ArgumentError] Occurs for data that cannot be decoded.
30
+ def self.from_pkt(message)
31
+ buf = StringIO.new(message)
32
+ magic = buf.read(4)
33
+ raise ArgumentError, 'Invalid magic.' unless magic == Bitcoin.chain_params.magic_head.htb
34
+ command = buf.read(12).delete("\x00")
35
+ length = buf.read(4).unpack1('V')
36
+ checksum = buf.read(4)
37
+ payload = buf.read(length)
38
+ raise ArgumentError, 'Checksum do not match.' unless checksum == Bitcoin.double_sha256(payload)[0...4]
39
+ Bitcoin::Message.decode(command, payload&.bth)
40
+ end
41
+
26
42
  end
27
43
 
28
44
  end
@@ -19,8 +19,8 @@ module Bitcoin
19
19
 
20
20
  def self.parse_from_payload(payload)
21
21
  buf = StringIO.new(payload)
22
- type = buf.read(1).unpack('C').first
23
- hash = buf.read(32).unpack('H*').first
22
+ type = buf.read(1).unpack1('C')
23
+ hash = buf.read(32).unpack1('H*')
24
24
  count = Bitcoin.unpack_var_int_from_io(buf)
25
25
  headers = count.times.map{buf.read(32).bth}
26
26
  self.new(type, hash, headers)
@@ -21,7 +21,7 @@ module Bitcoin
21
21
 
22
22
  def self.parse_from_payload(payload)
23
23
  buf = StringIO.new(payload)
24
- type = buf.read(1).unpack("C").first
24
+ type = buf.read(1).unpack1("C")
25
25
  hash = buf.read(32).bth
26
26
  header = buf.read(32).bth
27
27
  count = Bitcoin.unpack_var_int_from_io(buf)
@@ -19,7 +19,7 @@ module Bitcoin
19
19
 
20
20
  def self.parse_from_payload(payload)
21
21
  buf = StringIO.new(payload)
22
- type = buf.read(1).unpack("C").first
22
+ type = buf.read(1).unpack1("C")
23
23
  hash = buf.read(32).bth
24
24
  len = Bitcoin.unpack_var_int_from_io(buf)
25
25
  filter = buf.read(len).bth
@@ -15,7 +15,7 @@ module Bitcoin
15
15
  end
16
16
 
17
17
  def self.parse_from_payload(payload)
18
- new(payload.unpack('Q').first)
18
+ new(payload.unpack1('Q'))
19
19
  end
20
20
 
21
21
  def to_payload
@@ -23,9 +23,9 @@ module Bitcoin
23
23
  buf = StringIO.new(payload)
24
24
  filter_count = Bitcoin.unpack_var_int_from_io(buf)
25
25
  filter = buf.read(filter_count).unpack('C*')
26
- func_count = buf.read(4).unpack('V').first
27
- tweak = buf.read(4).unpack('V').first
28
- flag = buf.read(1).unpack('C').first
26
+ func_count = buf.read(4).unpack1('V')
27
+ tweak = buf.read(4).unpack1('V')
28
+ flag = buf.read(1).unpack1('C')
29
29
  FilterLoad.new(Bitcoin::BloomFilter.new(filter, func_count, tweak), flag)
30
30
  end
31
31
 
@@ -22,7 +22,7 @@ module Bitcoin
22
22
  def self.parse_from_payload(payload)
23
23
  buf = StringIO.new(payload)
24
24
  header = Bitcoin::BlockHeader.parse_from_payload(buf.read(80))
25
- nonce = buf.read(8).unpack('q*').first
25
+ nonce = buf.read(8).unpack1('q*')
26
26
  short_ids_len = Bitcoin.unpack_var_int_from_io(buf)
27
27
  short_ids = short_ids_len.times.map do
28
28
  buf.read(6).reverse.bth.to_i(16)
@@ -26,7 +26,7 @@ module Bitcoin
26
26
  # parse inventory payload
27
27
  def self.parse_from_payload(payload)
28
28
  raise Error, 'invalid inventory size.' if payload.bytesize != 36
29
- identifier = payload[0..4].unpack('V').first
29
+ identifier = payload[0..4].unpack1('V')
30
30
  hash = payload[4..-1].bth # internal byte order
31
31
  new(identifier, hash)
32
32
  end
@@ -20,7 +20,7 @@ module Bitcoin
20
20
  m = new
21
21
  buf = StringIO.new(payload)
22
22
  m.header = Bitcoin::BlockHeader.parse_from_payload(buf.read(80))
23
- m.tx_count = buf.read(4).unpack('V').first
23
+ m.tx_count = buf.read(4).unpack1('V')
24
24
  hash_count = Bitcoin.unpack_var_int_from_io(buf)
25
25
  hash_count.times do
26
26
  m.hashes << buf.read(32).bth
@@ -1,10 +1,16 @@
1
1
  require 'ipaddr'
2
+ require 'base32'
2
3
 
3
4
  module Bitcoin
4
5
  module Message
5
6
 
7
+ NETWORK_ID = {ipv4: 0x01, ipv6: 0x02, tor_v2: 0x03, tor_v3: 0x04, i2p: 0x05, cjdns: 0x06}
8
+ INTERNAL_IN_IPV6_PREFIX = "fd6b:88c0:8724"
9
+
6
10
  class NetworkAddr
7
11
 
12
+ TYPE = {legacy: 0x01, addr_v2: 0x02}
13
+
8
14
  # unix time.
9
15
  # Nodes advertising their own IP address set this to the current time.
10
16
  # Nodes advertising IP addresses they’ve connected to set this to the last time they connected to that node.
@@ -14,47 +20,164 @@ module Bitcoin
14
20
  # The services the node advertised in its version message.
15
21
  attr_accessor :services
16
22
 
17
- attr_accessor :ip_addr # IPAddr
23
+ attr_accessor :net # network ID that defined by BIP-155
24
+
25
+ # Network address. The interpretation depends on networkID.
26
+ # If ipv4 or ipv6 this field is a IPAddr object, otherwise hex string.
27
+ attr_accessor :addr
18
28
 
19
29
  attr_accessor :port
20
30
 
21
31
  attr_reader :skip_time
22
32
 
23
- def initialize(ip: '127.0.0.1', port: Bitcoin.chain_params.default_port, services: DEFAULT_SERVICE_FLAGS, time: Time.now.to_i)
33
+ def initialize(ip: '127.0.0.1', port: Bitcoin.chain_params.default_port,
34
+ services: DEFAULT_SERVICE_FLAGS, time: Time.now.to_i, net: NETWORK_ID[:ipv4])
24
35
  @time = time
25
- @ip_addr = IPAddr.new(ip)
26
36
  @port = port
27
37
  @services = services
38
+ @net = net
39
+ case net
40
+ when NETWORK_ID[:ipv4], NETWORK_ID[:ipv6]
41
+ @addr = IPAddr.new(ip) if ip
42
+ end
28
43
  end
29
44
 
30
- def self.parse_from_payload(payload)
31
- buf = payload.is_a?(String) ? StringIO.new(payload) : payload
32
- has_time = buf.size > 26
33
- addr = new(time: nil)
34
- addr.time = buf.read(4).unpack('V').first if has_time
35
- addr.services = buf.read(8).unpack('Q').first
36
- addr.ip_addr = IPAddr::new_ntoh(buf.read(16))
37
- addr.port = buf.read(2).unpack('n').first
38
- addr
45
+ # Parse addr payload
46
+ # @param [String] payload payload of addr
47
+ # @param [Integer] type Address format type
48
+ # @return [NetworkAddr]
49
+ def self.parse_from_payload(payload, type: TYPE[:legacy])
50
+ case type
51
+ when TYPE[:legacy]
52
+ load_legacy_payload(payload)
53
+ when TYPE[:addr_v2]
54
+ load_addr_v2_payload(payload)
55
+ else
56
+ raise Bitcoin::Message::Error, "Unknown type: #{type}."
57
+ end
39
58
  end
40
59
 
41
60
  def self.local_addr
42
61
  addr = new
43
- addr.ip_addr = IPAddr.new('127.0.0.1')
62
+ addr.addr = IPAddr.new('127.0.0.1')
44
63
  addr.port = Bitcoin.chain_params.default_port
45
64
  addr.services = DEFAULT_SERVICE_FLAGS
46
65
  addr
47
66
  end
48
67
 
49
- def ip
50
- ip_addr.ipv4_mapped? ? ip_addr.native : ip_addr.to_s
68
+ # Show addr string. e.g 127.0.0.1
69
+ def addr_string
70
+ case net
71
+ when NETWORK_ID[:ipv4]
72
+ addr.native
73
+ when NETWORK_ID[:ipv6]
74
+ if addr.to_s.start_with?(INTERNAL_IN_IPV6_PREFIX)
75
+ Base32.encode(addr.hton[6..-1]).downcase.delete('=') + ".internal"
76
+ else
77
+ addr.to_s
78
+ end
79
+ when NETWORK_ID[:tor_v2]
80
+ Base32.encode(addr.htb).downcase + ".onion"
81
+ when NETWORK_ID[:tor_v3]
82
+ # TORv3 onion_address = base32(PUBKEY | CHECKSUM | VERSION) + ".onion"
83
+ pubkey = addr.htb
84
+ checksum = OpenSSL::Digest.new('SHA3-256').digest('.onion checksum' + pubkey + "\x03")
85
+ Base32.encode(pubkey + checksum[0...2] + "\x03").downcase + ".onion"
86
+ when NETWORK_ID[:i2p]
87
+ Base32.encode(addr.htb).downcase.delete('=') + ".b32.i2p"
88
+ when NETWORK_ID[:cjdns]
89
+ addr.to_s
90
+ end
91
+ end
92
+
93
+ def to_payload(skip_time = false, type: TYPE[:legacy])
94
+ case type
95
+ when TYPE[:legacy]
96
+ legacy_payload(skip_time)
97
+ when TYPE[:addr_v2]
98
+ v2_payload
99
+ else
100
+ raise Bitcoin::Message::Error, "Unknown type: #{type}."
101
+ end
51
102
  end
52
103
 
53
- def to_payload(skip_time = false)
104
+ # Load addr payload with legacy format.
105
+ def self.load_legacy_payload(payload)
106
+ buf = payload.is_a?(String) ? StringIO.new(payload) : payload
107
+ has_time = buf.size > 26
108
+ addr = NetworkAddr.new(time: nil)
109
+ addr.time = buf.read(4).unpack1('V') if has_time
110
+ addr.services = buf.read(8).unpack1('Q')
111
+ addr.addr = IPAddr::new_ntoh(buf.read(16))
112
+ addr.port = buf.read(2).unpack1('n')
113
+ addr
114
+ end
115
+
116
+ # Load addr payload with addr v2 format.
117
+ def self.load_addr_v2_payload(payload)
118
+ buf = payload.is_a?(String) ? StringIO.new(payload) : payload
119
+ addr = NetworkAddr.new(time: buf.read(4).unpack1('V'))
120
+ addr.services = Bitcoin.unpack_var_int_from_io(buf)
121
+ addr.net = buf.read(1).unpack1('C')
122
+ raise Bitcoin::Message::Error, "Unknown network id: #{addr.net}" unless NETWORK_ID.value?(addr.net)
123
+ addr_len = Bitcoin.unpack_var_int_from_io(buf)
124
+ addr.addr = case addr.net
125
+ when NETWORK_ID[:ipv4]
126
+ raise Bitcoin::Message::Error, "Invalid IPv4 address." unless addr_len == 4
127
+ IPAddr::new_ntoh(buf.read(addr_len))
128
+ when NETWORK_ID[:ipv6]
129
+ raise Bitcoin::Message::Error, "Invalid IPv6 address." unless addr_len == 16
130
+ a = IPAddr::new_ntoh(buf.read(addr_len))
131
+ raise Bitcoin::Message::Error, "Invalid IPv6 address." if a.ipv4_mapped?
132
+ a
133
+ when NETWORK_ID[:tor_v2]
134
+ raise Bitcoin::Message::Error, "Invalid Tor v2 address." unless addr_len == 10
135
+ buf.read(addr_len).bth
136
+ when NETWORK_ID[:tor_v3]
137
+ raise Bitcoin::Message::Error, "Invalid Tor v3 address." unless addr_len == 32
138
+ buf.read(addr_len).bth
139
+ when NETWORK_ID[:i2p]
140
+ raise Bitcoin::Message::Error, "Invalid I2P address." unless addr_len == 32
141
+ buf.read(addr_len).bth
142
+ when NETWORK_ID[:cjdns]
143
+ raise Bitcoin::Message::Error, "Invalid CJDNS address." unless addr_len == 16
144
+ a = IPAddr::new_ntoh(buf.read(addr_len))
145
+ raise Bitcoin::Message::Error, "Invalid CJDNS address." unless a.to_s.start_with?('fc00:')
146
+ a
147
+ end
148
+ addr.port = buf.read(2).unpack1('n')
149
+ addr
150
+ end
151
+
152
+ def legacy_payload(skip_time)
54
153
  p = ''
55
154
  p << [time].pack('V') unless skip_time
56
- addr = ip_addr.ipv4? ? ip_addr.ipv4_mapped : ip_addr
57
- p << [services].pack('Q') << addr.hton << [port].pack('n')
155
+ ip = addr.ipv4? ? addr.ipv4_mapped : addr
156
+ p << [services].pack('Q') << ip.hton << [port].pack('n')
157
+ end
158
+
159
+ def v2_payload
160
+ p = [time].pack('V')
161
+ p << Bitcoin.pack_var_int(services)
162
+ p << [net].pack('C')
163
+ case net
164
+ when NETWORK_ID[:ipv4]
165
+ p << Bitcoin.pack_var_int(4)
166
+ p << addr.to_i.to_s(16).htb
167
+ when NETWORK_ID[:ipv6]
168
+ p << Bitcoin.pack_var_int(16)
169
+ p << addr.hton
170
+ when NETWORK_ID[:tor_v2]
171
+ p << Bitcoin.pack_var_int(10)
172
+ when NETWORK_ID[:tor_v3]
173
+ p << Bitcoin.pack_var_int(32)
174
+ when NETWORK_ID[:i2p]
175
+ p << Bitcoin.pack_var_int(32)
176
+ when NETWORK_ID[:cjdns]
177
+ p << Bitcoin.pack_var_int(16)
178
+ end
179
+ p << [port].pack('n')
180
+ p
58
181
  end
59
182
 
60
183
  end
@@ -14,7 +14,7 @@ module Bitcoin
14
14
  end
15
15
 
16
16
  def self.parse_from_payload(payload)
17
- new(payload.unpack('Q').first)
17
+ new(payload.unpack1('Q'))
18
18
  end
19
19
 
20
20
  def to_payload
@@ -14,7 +14,7 @@ module Bitcoin
14
14
  end
15
15
 
16
16
  def self.parse_from_payload(payload)
17
- new(payload.unpack('Q').first)
17
+ new(payload.unpack1('Q'))
18
18
  end
19
19
 
20
20
  def to_payload
@@ -0,0 +1,13 @@
1
+ module Bitcoin
2
+ module Message
3
+ class SendAddrV2 < Base
4
+
5
+ COMMAND = 'sendaddrv2'
6
+
7
+ def to_payload
8
+ ''
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -21,8 +21,8 @@ module Bitcoin
21
21
 
22
22
  def self.parse_from_payload(payload)
23
23
  buf = StringIO.new(payload)
24
- mode = buf.read(1).unpack('c').first
25
- version = buf.read(8).unpack('Q').first
24
+ mode = buf.read(1).unpack1('c')
25
+ version = buf.read(8).unpack1('Q')
26
26
  new(mode, version)
27
27
  end
28
28
 
@@ -16,7 +16,7 @@ module Bitcoin
16
16
  end
17
17
 
18
18
  def self.parse_from_payload(payload)
19
- tx = Bitcoin::Tx.parse_from_payload(payload)
19
+ tx = Bitcoin::Tx.parse_from_payload(payload, strict: true)
20
20
  new(tx, tx.witness?)
21
21
  end
22
22