tapyrus 0.2.1 → 0.2.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0742065204db9e10454dbdc9afc4401be70659e9998376d944b4e0f61a75cf27
4
- data.tar.gz: 578bd756abb67374d62951526638c9328c3f9e70f9925cd7e3f16f079093843b
3
+ metadata.gz: a1abedf80b636e2bfd80db4617ca9f92e703148f07e59c08b14860c13ac57689
4
+ data.tar.gz: 21b478051eb01354e410475e15761f24773fe164fcd9abf3307acb231b5c8ae3
5
5
  SHA512:
6
- metadata.gz: 584cf7b8ce40aeb94ecce56c393ab52805b5af76bf91439f5f1b39cf28ce15a723bc2b37862773e5c1cd994cb6d72f270b4f77c828708384e9fe70990efbd19c
7
- data.tar.gz: cdc17bfad7962a3900ad567c07d0d53550f0db4eacf8b10c80e76017206aeed702c336fee9a7283502ae3ebdcf68de6da45e90a22ea0044e22e80839fa6cd43d
6
+ metadata.gz: 61c8cc2439074666abffc6ff85ffaa40811e8e0992edb19846876dabf9a300f4822af522a6f621271f310a9b1ea3f9429f30e86452d29b0f2216052568a72a6a
7
+ data.tar.gz: f3845015c030c7ccf915700eb45ac2f743c3f0c19dbe077c72c4afe19327252d1f3a64ffb29c1f5790997c8f6f2317930ab5ae99a894687a86f6381154c5429d
@@ -76,7 +76,7 @@ module Schnorr
76
76
  # @return (Integer) digest e.
77
77
  def create_challenge(x, p, message)
78
78
  r_x = ECDSA::Format::IntegerOctetString.encode(x, GROUP.byte_length)
79
- p_str= ECDSA::Format::PointOctetString.encode(p, compression:true)
79
+ p_str= p.to_hex.htb
80
80
  (ECDSA.normalize_digest(Digest::SHA256.digest(r_x + p_str + message), GROUP.bit_length)) % GROUP.order
81
81
  end
82
82
 
@@ -48,8 +48,10 @@ module Tapyrus
48
48
  autoload :KeyPath, 'tapyrus/key_path'
49
49
  autoload :SLIP39, 'tapyrus/slip39'
50
50
  autoload :Color, 'tapyrus/script/color'
51
+ autoload :Errors, 'tapyrus/errors'
51
52
 
52
53
  require_relative 'tapyrus/constants'
54
+ require_relative 'tapyrus/ext/ecdsa'
53
55
 
54
56
  extend Util
55
57
 
@@ -207,11 +209,4 @@ module Tapyrus
207
209
  end
208
210
  end
209
211
 
210
- class ::ECDSA::Signature
211
- # convert signature to der string.
212
- def to_der
213
- ECDSA::Format::SignatureDerString.encode(self)
214
- end
215
- end
216
-
217
212
  end
@@ -1,5 +1,6 @@
1
1
  module Tapyrus
2
2
  class Block
3
+ include Tapyrus::Util
3
4
 
4
5
  attr_accessor :header
5
6
  attr_accessor :transactions
@@ -32,14 +33,9 @@ module Tapyrus
32
33
  end
33
34
 
34
35
  # return this block height. block height is included in coinbase.
35
- # if block version under 1, height does not include in coinbase, so return nil.
36
+ # @return [Integer] block height.
36
37
  def height
37
- return nil if header.features < 2
38
- coinbase_tx = transactions[0]
39
- return nil unless coinbase_tx.coinbase_tx?
40
- buf = StringIO.new(coinbase_tx.inputs[0].script_sig.to_payload)
41
- len = Tapyrus.unpack_var_int_from_io(buf)
42
- buf.read(len).reverse.bth.to_i(16)
38
+ transactions.first.in.first.out_point.index
43
39
  end
44
40
 
45
41
  end
@@ -3,61 +3,69 @@ module Tapyrus
3
3
  # Block Header
4
4
  class BlockHeader
5
5
  include Tapyrus::HexConverter
6
+ extend Tapyrus::Util
7
+ include Tapyrus::Util
8
+
9
+ X_FILED_TYPES = {none: 0, aggregate_pubkey: 1}
6
10
 
7
11
  attr_accessor :features
8
12
  attr_accessor :prev_hash
9
13
  attr_accessor :merkle_root
10
- attr_accessor :time # unix timestamp
11
- attr_accessor :bits
12
- attr_accessor :nonce
14
+ attr_accessor :im_merkle_root # merkel root of immulable merkle tree which consist of immutable txid.
15
+ attr_accessor :time # unix timestamp
16
+ attr_accessor :x_field_type
17
+ attr_accessor :x_field
18
+ attr_accessor :proof
13
19
 
14
- def initialize(features, prev_hash, merkle_root, time, bits, nonce)
20
+ def initialize(features, prev_hash, merkle_root, im_merkle_root, time, x_field_type, x_field, proof = nil)
15
21
  @features = features
16
22
  @prev_hash = prev_hash
17
23
  @merkle_root = merkle_root
24
+ @im_merkle_root = im_merkle_root
18
25
  @time = time
19
- @bits = bits
20
- @nonce = nonce
26
+ @x_field_type = x_field_type
27
+ @x_field = x_field
28
+ @proof = proof
21
29
  end
22
30
 
23
31
  def self.parse_from_payload(payload)
24
- features, prev_hash, merkle_root, time, bits, nonce = payload.unpack('Va32a32VVV')
25
- new(features, prev_hash.bth, merkle_root.bth, time, bits, nonce)
26
- end
27
-
28
- def to_payload
29
- [features, prev_hash.htb, merkle_root.htb, time, bits, nonce].pack('Va32a32VVV')
32
+ buf = payload.is_a?(String) ? StringIO.new(payload) : payload
33
+ features, prev_hash, merkle_root, im_merkle_root, time, x_filed_type = buf.read(105).unpack('Va32a32a32Vc')
34
+ x_field = buf.read(unpack_var_int_from_io(buf)) unless x_filed_type == X_FILED_TYPES[:none]
35
+ proof = buf.read(unpack_var_int_from_io(buf))
36
+ new(features, prev_hash.bth, merkle_root.bth, im_merkle_root.bth, time, x_filed_type, x_field ? x_field.bth : x_field, proof.bth)
30
37
  end
31
38
 
32
- # compute difficulty target from bits.
33
- def difficulty_target
34
- exponent = ((bits >> 24) & 0xff)
35
- mantissa = bits & 0x7fffff
36
- mantissa *= -1 if (bits & 0x800000) > 0
37
- (mantissa * 2 ** (8 * (exponent - 3)))
39
+ def to_payload(skip_proof = false)
40
+ payload = [features, prev_hash.htb, merkle_root.htb, im_merkle_root.htb, time, x_field_type].pack('Va32a32a32Vc')
41
+ payload << pack_var_string(x_field.htb) unless x_field_type == X_FILED_TYPES[:none]
42
+ payload << pack_var_string(proof.htb) if proof && !skip_proof
43
+ payload
38
44
  end
39
45
 
40
- def hash
41
- calc_hash.to_i(16)
46
+ # Calculate hash using sign which does not contains proof.
47
+ # @return [String] hash of block without proof.
48
+ def hash_for_sign
49
+ Tapyrus.double_sha256(to_payload(true)).bth
42
50
  end
43
51
 
52
+ # Calculate block hash
53
+ # @return [String] hash of block with hex format.
44
54
  def block_hash
45
- calc_hash
55
+ Tapyrus.double_sha256(to_payload).bth
46
56
  end
47
57
 
48
58
  # block hash(big endian)
59
+ # @return [String] block id which is reversed version of block hash.
49
60
  def block_id
50
61
  block_hash.rhex
51
62
  end
52
63
 
53
64
  # evaluate block header
54
- def valid?
55
- valid_pow? && valid_timestamp?
56
- end
57
-
58
- # evaluate valid proof of work.
59
- def valid_pow?
60
- block_id.hex < difficulty_target
65
+ # @param [String] agg_pubkey aggregated public key for signers with hex format.
66
+ # @return [Boolean] result.
67
+ def valid?(agg_pubkey)
68
+ valid_timestamp? && valid_proof?(agg_pubkey) && valid_x_field?
61
69
  end
62
70
 
63
71
  # evaluate valid timestamp.
@@ -66,22 +74,42 @@ module Tapyrus
66
74
  time <= Time.now.to_i + Tapyrus::MAX_FUTURE_BLOCK_TIME
67
75
  end
68
76
 
69
- # compute chain work of this block.
70
- # @return [Integer] a chain work.
71
- def work
72
- target = difficulty_target
73
- return 0 if target < 1
74
- 115792089237316195423570985008687907853269984665640564039457584007913129639936.div(target + 1) # 115792089237316195423570985008687907853269984665640564039457584007913129639936 is 2**256
77
+ # Check whether proof is valid.
78
+ # @param [String] agg_pubkey aggregated public key for signers with hex format.
79
+ # @return [Boolean] Return true if proof is valid, otherwise return false.
80
+ def valid_proof?(agg_pubkey)
81
+ pubkey = Tapyrus::Key.new(pubkey: agg_pubkey)
82
+ msg = hash_for_sign.htb
83
+ pubkey.verify(proof.htb, msg, algo: :schnorr)
84
+ end
85
+
86
+ # Check whether x_field is valid.
87
+ # @return [Boolean] if valid return true, otherwise false
88
+ def valid_x_field?
89
+ case x_field_type
90
+ when X_FILED_TYPES[:none] then
91
+ x_field.nil?
92
+ when X_FILED_TYPES[:aggregate_pubkey] then
93
+ Tapyrus::Key.new(pubkey: x_field).fully_valid_pubkey?
94
+ else
95
+ false
96
+ end
97
+ end
98
+
99
+ # Check this header contains upgrade aggregated publiec key.
100
+ # @return [Boolean] if contains return true, otherwise false.
101
+ def upgrade_agg_pubkey?
102
+ x_field_type == X_FILED_TYPES[:aggregate_pubkey]
75
103
  end
76
104
 
77
105
  def ==(other)
78
106
  other && other.to_payload == to_payload
79
107
  end
80
108
 
81
- private
82
-
83
- def calc_hash
84
- Tapyrus.double_sha256(to_payload).bth
109
+ # get bytesize.
110
+ # @return [Integer] bytesize.
111
+ def size
112
+ to_payload.bytesize
85
113
  end
86
114
 
87
115
  end
@@ -33,12 +33,10 @@ module Tapyrus
33
33
  attr_reader :bip34_height
34
34
  attr_reader :proof_of_work_limit
35
35
  attr_reader :dns_seeds
36
- attr_reader :genesis
37
36
  attr_reader :bip44_coin_type
38
37
 
39
38
  attr_accessor :dust_relay_fee
40
39
 
41
-
42
40
  # production genesis
43
41
  def self.prod
44
42
  init('prod')
@@ -57,14 +55,6 @@ module Tapyrus
57
55
  network == 'dev'
58
56
  end
59
57
 
60
- def genesis_block
61
- header = Tapyrus::BlockHeader.new(
62
- genesis['version'], genesis['prev_hash'].rhex, genesis['merkle_root'].rhex,
63
- genesis['time'], genesis['bits'], genesis['nonce'])
64
- Tapyrus::Block.new(header)
65
- end
66
-
67
-
68
58
  def self.init(name)
69
59
  i = YAML.load(File.open("#{__dir__}/chainparams/#{name}.yml"))
70
60
  i.dust_relay_fee ||= Tapyrus::DUST_RELAY_TX_FEE
@@ -25,15 +25,5 @@ retarget_time: 1209600 # 2 weeks
25
25
  target_spacing: 600 # block interval
26
26
  max_money: 21000000
27
27
  bip34_height: 227931
28
- genesis_hash: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
29
- proof_of_work_limit: 0x1d00ffff
30
28
  dns_seeds:
31
- genesis:
32
- hash: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
33
- merkle_root: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
34
- time: 1296688602
35
- nonce: 414098458
36
- bits: 0x1d00ffff
37
- version: 1
38
- prev_hash: "0000000000000000000000000000000000000000000000000000000000000000"
39
29
  bip44_coin_type: 1
@@ -25,14 +25,5 @@ retarget_time: 1209600 # 2 weeks
25
25
  target_spacing: 600 # block interval
26
26
  max_money: 21000000
27
27
  bip34_height: 227931
28
- proof_of_work_limit: 0x1d00ffff
29
28
  dns_seeds:
30
- genesis:
31
- hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
32
- merkle_root: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
33
- time: 1231006505
34
- nonce: 2083236893
35
- bits: 0x1d00ffff
36
- version: 1
37
- prev_hash: "0000000000000000000000000000000000000000000000000000000000000000"
38
29
  bip44_coin_type: 0
@@ -0,0 +1,17 @@
1
+ module Tapyrus
2
+ module Errors
3
+ module Messages
4
+
5
+ INVALID_PUBLIC_KEY = 'Invalid public key.'
6
+ INVALID_BIP32_PRIV_PREFIX = 'Invalid BIP32 private key prefix. prefix must be 0x00.'
7
+ INVALID_BIP32_FINGERPRINT = 'Invalid parent fingerprint.'
8
+ INVALID_BIP32_ZERO_INDEX = 'Invalid index. Depth 0 must have 0 index.'
9
+ INVALID_BIP32_ZERO_DEPTH = 'Invalid depth. Master key must have 0 depth.'
10
+ INVALID_BIP32_VERSION = 'An unsupported version byte was specified.'
11
+
12
+ INVALID_PRIV_KEY = 'Private key is not in range [1..n-1].'
13
+ INVALID_CHECKSUM = 'Invalid checksum.'
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ class ::ECDSA::Signature
2
+ # convert signature to der string.
3
+ def to_der
4
+ ECDSA::Format::SignatureDerString.encode(self)
5
+ end
6
+ end
7
+
8
+ class ::ECDSA::Point
9
+ def to_hex(compression = true)
10
+ ECDSA::Format::PointOctetString.encode(self, compression: compression).bth
11
+ end
12
+ end
13
+
14
+ module ::ECDSA::Format::PointOctetString
15
+
16
+ def self.decode(string, group, allow_hybrid: false)
17
+ string = string.dup.force_encoding('BINARY')
18
+
19
+ raise ECDSA::Format::DecodeError, 'Point octet string is empty.' if string.empty?
20
+
21
+ case string[0].ord
22
+ when 0
23
+ check_length string, 1
24
+ return group.infinity
25
+ when 2
26
+ decode_compressed string, group, 0
27
+ when 3
28
+ decode_compressed string, group, 1
29
+ when 4
30
+ decode_uncompressed string, group
31
+ when 6..7
32
+ raise DecodeError, 'Unrecognized start byte for point octet string: 0x%x' % string[0].ord unless allow_hybrid
33
+ decode_uncompressed string, group if allow_hybrid
34
+ else
35
+ raise ECDSA::Format::DecodeError, 'Unrecognized start byte for point octet string: 0x%x' % string[0].ord
36
+ end
37
+ end
38
+
39
+ end
@@ -7,6 +7,9 @@ module Tapyrus
7
7
  class ExtKey
8
8
  include Tapyrus::HexConverter
9
9
 
10
+ MAX_DEPTH = 255
11
+ MASTER_FINGERPRINT = '00000000'
12
+
10
13
  attr_accessor :ver
11
14
  attr_accessor :depth
12
15
  attr_accessor :number
@@ -20,7 +23,7 @@ module Tapyrus
20
23
  ext_key = ExtKey.new
21
24
  ext_key.depth = ext_key.number = 0
22
25
  ext_key.parent_fingerprint = '00000000'
23
- l = Tapyrus.hmac_sha512('Bitcoin seed', seed.htb)
26
+ l = Tapyrus.hmac_sha512('Tapyrus seed', seed.htb)
24
27
  left = l[0..31].bth.to_i(16)
25
28
  raise 'invalid key' if left >= CURVE_ORDER || left == 0
26
29
  ext_key.key = Tapyrus::Key.new(priv_key: l[0..31].bth, key_type: Tapyrus::Key::TYPES[:compressed])
@@ -48,9 +51,7 @@ module Tapyrus
48
51
 
49
52
  # Base58 encoded extended private key
50
53
  def to_base58
51
- h = to_hex
52
- hex = h + Tapyrus.calc_checksum(h)
53
- Base58.encode(hex)
54
+ ExtPubkey.encode_base58(to_hex)
54
55
  end
55
56
 
56
57
  # get private key(hex)
@@ -141,19 +142,24 @@ module Tapyrus
141
142
  buf = StringIO.new(payload)
142
143
  ext_key = ExtKey.new
143
144
  ext_key.ver = buf.read(4).bth # version
144
- raise 'An unsupported version byte was specified.' unless ExtKey.support_version?(ext_key.ver)
145
+ raise ArgumentError, Errors::Messages::INVALID_BIP32_VERSION unless ExtKey.support_version?(ext_key.ver)
145
146
  ext_key.depth = buf.read(1).unpack('C').first
146
147
  ext_key.parent_fingerprint = buf.read(4).bth
147
148
  ext_key.number = buf.read(4).unpack('N').first
149
+ if ext_key.depth == 0
150
+ raise ArgumentError, Errors::Messages::INVALID_BIP32_FINGERPRINT unless ext_key.parent_fingerprint == ExtKey::MASTER_FINGERPRINT
151
+ raise ArgumentError, Errors::Messages::INVALID_BIP32_ZERO_INDEX if ext_key.number > 0
152
+ end
153
+ raise ArgumentError, Errors::Messages:: INVALID_BIP32_ZERO_DEPTH if ext_key.parent_fingerprint == ExtKey::MASTER_FINGERPRINT && ext_key.depth > 0
148
154
  ext_key.chain_code = buf.read(32)
149
- buf.read(1) # 0x00
155
+ raise ArgumentError, Errors::Messages::INVALID_BIP32_PRIV_PREFIX unless buf.read(1).bth == '00' # 0x00
150
156
  ext_key.key = Tapyrus::Key.new(priv_key: buf.read(32).bth, key_type: Tapyrus::Key::TYPES[:compressed])
151
157
  ext_key
152
158
  end
153
159
 
154
160
  # import private key from Base58 private key address
155
161
  def self.from_base58(address)
156
- ExtKey.parse_from_payload(Base58.decode(address).htb)
162
+ ExtKey.parse_from_payload(ExtPubkey.validate_checksum(address))
157
163
  end
158
164
 
159
165
  # get version bytes from purpose' value.
@@ -237,9 +243,14 @@ module Tapyrus
237
243
 
238
244
  # Base58 encoded extended pubkey
239
245
  def to_base58
240
- h = to_hex
241
- hex = h + Tapyrus.calc_checksum(h)
242
- Base58.encode(hex)
246
+ ExtPubkey.encode_base58(to_hex)
247
+ end
248
+
249
+ # Generate Base58 encoded key from BIP32 payload with hex format.
250
+ # @param [String] hex BIP32 payload with hex format.
251
+ # @return [String] Base58 encoded extended key.
252
+ def self.encode_base58(hex)
253
+ Base58.encode(hex + Tapyrus.calc_checksum(hex))
243
254
  end
244
255
 
245
256
  # whether hardened key.
@@ -260,7 +271,7 @@ module Tapyrus
260
271
  raise 'invalid key' if left >= CURVE_ORDER
261
272
  p1 = Tapyrus::Secp256k1::GROUP.generator.multiply_by_scalar(left)
262
273
  p2 = Tapyrus::Key.new(pubkey: pubkey, key_type: key_type).to_point
263
- new_key.pubkey = ECDSA::Format::PointOctetString.encode(p1 + p2, compression: true).bth
274
+ new_key.pubkey = (p1 + p2).to_hex
264
275
  new_key.chain_code = l[32..-1]
265
276
  new_key.ver = version
266
277
  new_key
@@ -293,19 +304,32 @@ module Tapyrus
293
304
  buf = StringIO.new(payload)
294
305
  ext_pubkey = ExtPubkey.new
295
306
  ext_pubkey.ver = buf.read(4).bth # version
296
- raise 'An unsupported version byte was specified.' unless ExtPubkey.support_version?(ext_pubkey.ver)
307
+ raise ArgumentError, Errors::Messages::INVALID_BIP32_VERSION unless ExtPubkey.support_version?(ext_pubkey.ver)
297
308
  ext_pubkey.depth = buf.read(1).unpack('C').first
298
309
  ext_pubkey.parent_fingerprint = buf.read(4).bth
299
310
  ext_pubkey.number = buf.read(4).unpack('N').first
311
+ if ext_pubkey.depth == 0
312
+ raise ArgumentError, Errors::Messages::INVALID_BIP32_FINGERPRINT unless ext_pubkey.parent_fingerprint == ExtKey::MASTER_FINGERPRINT
313
+ raise ArgumentError, Errors::Messages::INVALID_BIP32_ZERO_INDEX if ext_pubkey.number > 0
314
+ end
315
+ raise ArgumentError, Errors::Messages::INVALID_BIP32_ZERO_DEPTH if ext_pubkey.parent_fingerprint == ExtKey::MASTER_FINGERPRINT && ext_pubkey.depth > 0
300
316
  ext_pubkey.chain_code = buf.read(32)
301
- ext_pubkey.pubkey = buf.read(33).bth
317
+ ext_pubkey.pubkey = Tapyrus::Key.new(pubkey: buf.read(33).bth).pubkey
302
318
  ext_pubkey
303
319
  end
304
320
 
305
-
306
321
  # import pub key from Base58 private key address
307
322
  def self.from_base58(address)
308
- ExtPubkey.parse_from_payload(Base58.decode(address).htb)
323
+ ExtPubkey.parse_from_payload(ExtPubkey.validate_checksum(address))
324
+ end
325
+
326
+ # Validate address checksum and return payload.
327
+ # @param [String] BIP32 Base58 address
328
+ # @return [String] BIP32 payload with binary format
329
+ def self.validate_checksum(base58)
330
+ raw = Base58.decode(base58)
331
+ raise ArgumentError, Errors::Messages::INVALID_CHECKSUM unless Tapyrus.calc_checksum(raw[0...-8]) == raw[-8..-1]
332
+ raw[0...-8].htb
309
333
  end
310
334
 
311
335
  # get version bytes from purpose' value.
@@ -30,7 +30,7 @@ module Tapyrus
30
30
  # @param [Integer] key_type a key type which determine address type.
31
31
  # @param [Boolean] compressed [Deprecated] whether public key is compressed.
32
32
  # @return [Tapyrus::Key] a key object.
33
- def initialize(priv_key: nil, pubkey: nil, key_type: nil, compressed: true)
33
+ def initialize(priv_key: nil, pubkey: nil, key_type: nil, compressed: true, allow_hybrid: false)
34
34
  puts "[Warning] Use key_type parameter instead of compressed. compressed parameter removed in the future." if key_type.nil? && !compressed.nil? && pubkey.nil?
35
35
  if key_type
36
36
  @key_type = key_type
@@ -41,13 +41,14 @@ module Tapyrus
41
41
  @secp256k1_module = Tapyrus.secp_impl
42
42
  @priv_key = priv_key
43
43
  if @priv_key
44
- raise ArgumentError, 'private key is not on curve' unless validate_private_key_range(@priv_key)
44
+ raise ArgumentError, Errors::Messages::INVALID_PRIV_KEY unless validate_private_key_range(@priv_key)
45
45
  end
46
46
  if pubkey
47
47
  @pubkey = pubkey
48
48
  else
49
49
  @pubkey = generate_pubkey(priv_key, compressed: compressed) if priv_key
50
50
  end
51
+ raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless fully_valid_pubkey?(allow_hybrid)
51
52
  end
52
53
 
53
54
  # generate key pair
@@ -65,7 +66,7 @@ module Tapyrus
65
66
  data = hex[2...-8].htb
66
67
  checksum = hex[-8..-1]
67
68
  raise ArgumentError, 'invalid version' unless version == Tapyrus.chain_params.privkey_version
68
- raise ArgumentError, 'invalid checksum' unless Tapyrus.calc_checksum(version + data.bth) == checksum
69
+ raise ArgumentError, Errors::Messages::INVALID_CHECKSUM unless Tapyrus.calc_checksum(version + data.bth) == checksum
69
70
  key_len = data.bytesize
70
71
  if key_len == COMPRESSED_PUBLIC_KEY_SIZE && data[-1].unpack('C').first == 1
71
72
  key_type = TYPES[:compressed]
@@ -223,10 +224,10 @@ module Tapyrus
223
224
  end
224
225
 
225
226
  # fully validate whether this is a valid public key (more expensive than IsValid())
226
- def fully_valid_pubkey?
227
+ def fully_valid_pubkey?(allow_hybrid = false)
227
228
  return false unless valid_pubkey?
228
229
  begin
229
- point = ECDSA::Format::PointOctetString.decode(pubkey.htb, ECDSA::Group::Secp256k1)
230
+ point = ECDSA::Format::PointOctetString.decode(pubkey.htb, ECDSA::Group::Secp256k1, allow_hybrid: allow_hybrid)
230
231
  ECDSA::Group::Secp256k1.valid_public_key?(point)
231
232
  rescue ECDSA::Format::DecodeError
232
233
  false
@@ -19,7 +19,7 @@ module Tapyrus
19
19
 
20
20
  def self.parse_from_payload(payload)
21
21
  buf = StringIO.new(payload)
22
- header = Tapyrus::BlockHeader.parse_from_payload(buf.read(80))
22
+ header = Tapyrus::BlockHeader.parse_from_payload(buf)
23
23
  transactions = []
24
24
  unless buf.eof?
25
25
  tx_count = Tapyrus.unpack_var_int_from_io(buf)
@@ -21,7 +21,7 @@ module Tapyrus
21
21
 
22
22
  def self.parse_from_payload(payload)
23
23
  buf = StringIO.new(payload)
24
- header = Tapyrus::BlockHeader.parse_from_payload(buf.read(80))
24
+ header = Tapyrus::BlockHeader.parse_from_payload(buf)
25
25
  nonce = buf.read(8).unpack('q*').first
26
26
  short_ids_len = Tapyrus.unpack_var_int_from_io(buf)
27
27
  short_ids = short_ids_len.times.map do
@@ -19,7 +19,7 @@ module Tapyrus
19
19
  header_count = Tapyrus.unpack_var_int_from_io(buf)
20
20
  h = new
21
21
  header_count.times do
22
- h.headers << Tapyrus::BlockHeader.parse_from_payload(buf.read(80))
22
+ h.headers << Tapyrus::BlockHeader.parse_from_payload(buf)
23
23
  buf.read(1) # read tx count 0x00 (headers message doesn't include any tx.)
24
24
  end
25
25
  h
@@ -19,7 +19,7 @@ module Tapyrus
19
19
  def self.parse_from_payload(payload)
20
20
  m = new
21
21
  buf = StringIO.new(payload)
22
- m.header = Tapyrus::BlockHeader.parse_from_payload(buf.read(80))
22
+ m.header = Tapyrus::BlockHeader.parse_from_payload(buf)
23
23
  m.tx_count = buf.read(4).unpack('V').first
24
24
  hash_count = Tapyrus.unpack_var_int_from_io(buf)
25
25
  hash_count.times do
@@ -11,7 +11,6 @@ module Tapyrus
11
11
  best_block = node.chain.latest_block
12
12
  h[:headers] = best_block.height
13
13
  h[:bestblockhash] = best_block.header.block_id
14
- h[:chainwork] = best_block.header.work
15
14
  h[:mediantime] = node.chain.mtp(best_block.block_hash)
16
15
  h
17
16
  end
@@ -32,12 +31,14 @@ module Tapyrus
32
31
  hash: block_id,
33
32
  height: entry.height,
34
33
  features: entry.header.features,
35
- featuresHex: entry.header.features.to_even_length_hex,
34
+ featuresHex: entry.header.features.to_even_length_hex.ljust(8, '0'),
36
35
  merkleroot: entry.header.merkle_root.rhex,
36
+ immutablemerkleroot: entry.header.im_merkle_root.rhex,
37
37
  time: entry.header.time,
38
38
  mediantime: node.chain.mtp(block_hash),
39
- nonce: entry.header.nonce,
40
- bits: entry.header.bits.to_even_length_hex,
39
+ xfield_type: entry.header.x_field_type,
40
+ xfield: entry.header.x_field,
41
+ proof: entry.header.proof,
41
42
  previousblockhash: entry.prev_hash.rhex,
42
43
  nextblockhash: node.chain.next_hash(block_hash).rhex
43
44
  }
@@ -64,7 +64,7 @@ module Tapyrus
64
64
  response = http.request(request)
65
65
  body = response.body
66
66
  response = Tapyrus::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
67
- raise response['error'].to_s if response['error']
67
+ raise response['error'].to_json if response['error']
68
68
  response['result']
69
69
  end
70
70
 
@@ -32,12 +32,16 @@ module Tapyrus
32
32
  # @param [String] pubkey a public key with hex format.
33
33
  # @param [String] digest a message digest with binary format to be verified.
34
34
  # @return [Boolean] if check is passed return true, otherwise false.
35
- def verify_sig(sig, pubkey, digest)
35
+ def verify_sig(sig, pubkey, digest, allow_hybrid: false)
36
36
  key_type = pubkey.start_with?('02') || pubkey.start_with?('03') ? Key::TYPES[:compressed] : Key::TYPES[:uncompressed]
37
37
  sig = sig.htb
38
38
  algo = sig.bytesize == 64 ? :schnorr : :ecdsa
39
- key = Key.new(pubkey: pubkey, key_type: key_type)
40
- key.verify(sig, digest, algo: algo)
39
+ begin
40
+ key = Key.new(pubkey: pubkey, key_type: key_type, allow_hybrid: allow_hybrid)
41
+ key.verify(sig, digest, algo: algo)
42
+ rescue Exception
43
+ false
44
+ end
41
45
  end
42
46
 
43
47
  def check_locktime(locktime)
@@ -12,8 +12,8 @@ module Tapyrus
12
12
  private_key = 1 + SecureRandom.random_number(GROUP.order - 1)
13
13
  public_key = GROUP.generator.multiply_by_scalar(private_key)
14
14
  privkey = ECDSA::Format::IntegerOctetString.encode(private_key, 32)
15
- pubkey = ECDSA::Format::PointOctetString.encode(public_key, compression: compressed)
16
- [privkey.bth, pubkey.bth]
15
+ pubkey = public_key.to_hex(compressed)
16
+ [privkey.bth, pubkey]
17
17
  end
18
18
 
19
19
  # generate tapyrus key object
@@ -24,7 +24,7 @@ module Tapyrus
24
24
 
25
25
  def generate_pubkey(privkey, compressed: true)
26
26
  public_key = ECDSA::Group::Secp256k1.generator.multiply_by_scalar(privkey.to_i(16))
27
- ECDSA::Format::PointOctetString.encode(public_key, compression: compressed).bth
27
+ public_key.to_hex(compressed)
28
28
  end
29
29
 
30
30
  # sign data.
@@ -36,7 +36,7 @@ module Tapyrus
36
36
 
37
37
  # whether genesis block
38
38
  def genesis?
39
- Tapyrus.chain_params.genesis_block.header == header
39
+ height == 0
40
40
  end
41
41
 
42
42
  # @param [String] payload a payload with binary format.
@@ -44,7 +44,7 @@ module Tapyrus
44
44
  buf = StringIO.new(payload)
45
45
  len = Tapyrus.unpack_var_int_from_io(buf)
46
46
  height = buf.read(len).reverse.bth.to_i(16)
47
- new(Tapyrus::BlockHeader.parse_from_payload(buf.read(80)), height)
47
+ new(Tapyrus::BlockHeader.parse_from_payload(buf), height)
48
48
  end
49
49
 
50
50
  # build next block +StoredBlock+ instance.
@@ -58,14 +58,64 @@ module Tapyrus
58
58
  db.get(KEY_PREFIX[:entry] + hash)
59
59
  end
60
60
 
61
+ # Save entry.
62
+ # @param [Tapyrus::Store::ChainEntry]
61
63
  def save_entry(entry)
62
64
  db.batch do
63
65
  db.put(entry.key ,entry.to_payload)
64
66
  db.put(height_key(entry.height), entry.block_hash)
67
+ add_agg_pubkey(entry.height == 0 ? 0 : entry.height + 1, entry.header.x_field) if entry.header.upgrade_agg_pubkey?
65
68
  connect_entry(entry)
66
69
  end
67
70
  end
68
71
 
72
+ # Add aggregated public key.
73
+ # @param [Integer] activate_height
74
+ # @param [String] agg_pubkey aggregated public key with hex format.
75
+ def add_agg_pubkey(activate_height, agg_pubkey)
76
+ payload = activate_height.to_even_length_hex + agg_pubkey
77
+ index = latest_agg_pubkey_index
78
+ next_index = (index.nil? ? 0 : index + 1).to_even_length_hex
79
+ db.batch do
80
+ db.put(KEY_PREFIX[:agg_pubkey] + next_index, payload)
81
+ db.put(KEY_PREFIX[:latest_agg_pubkey], next_index)
82
+ end
83
+ end
84
+
85
+ # Get aggregated public key by specifying +index+.
86
+ # @param [Integer] index
87
+ # @return [Array] tupple of activate height and aggregated public key.
88
+ def agg_pubkey(index)
89
+ payload = db.get(KEY_PREFIX[:agg_pubkey] + index.to_even_length_hex)
90
+ [payload[0...(payload.length - 66)].to_i(16), payload[(payload.length - 66)..-1]]
91
+ end
92
+
93
+ # Get aggregated public key by specifying block +height+.
94
+ # @param [Integer] height block height.
95
+ # @return [String] aggregated public key with hex format.
96
+ def agg_pubkey_with_height(height)
97
+ index = latest_agg_pubkey_index
98
+ index ||= 0
99
+ (index + 1).times do |i|
100
+ target = index - i
101
+ active_height, pubkey = agg_pubkey(target)
102
+ return pubkey unless active_height > height
103
+ end
104
+ end
105
+
106
+ # Get latest aggregated public key.
107
+ # @return [Array] aggregated public key with hex format.
108
+ def latest_agg_pubkey
109
+ agg_pubkey(latest_agg_pubkey_index)[1]
110
+ end
111
+
112
+ # Get aggregated public key list.
113
+ # @return [Array[Array]] list of public key and index
114
+ def agg_pubkeys
115
+ index = latest_agg_pubkey_index
116
+ (index + 1).times.map { |i| agg_pubkey(i) }
117
+ end
118
+
69
119
  def close
70
120
  db.close
71
121
  end
@@ -91,6 +141,14 @@ module Tapyrus
91
141
  db.put(KEY_PREFIX[:best], entry.block_hash)
92
142
  db.put(KEY_PREFIX[:next] + entry.prev_hash, entry.block_hash)
93
143
  end
144
+
145
+ # Get latest aggregated public key index.
146
+ # @return [Integer] key index
147
+ def latest_agg_pubkey_index
148
+ index = db.get(KEY_PREFIX[:latest_agg_pubkey])
149
+ index&.to_i(16)
150
+ end
151
+
94
152
  end
95
153
 
96
154
  end
@@ -3,10 +3,12 @@ module Tapyrus
3
3
  module Store
4
4
 
5
5
  KEY_PREFIX = {
6
- entry: 'e', # key: block hash, value: Tapyrus::Store::ChainEntry payload
7
- height: 'h', # key: block height, value: block hash.
8
- best: 'B', # value: best block hash.
9
- next: 'n' # key: block hash, value: A hash of the next block of the specified hash
6
+ entry: 'e', # key: block hash, value: Tapyrus::Store::ChainEntry payload
7
+ height: 'h', # key: block height, value: block hash.
8
+ best: 'B', # value: best block hash.
9
+ next: 'n', # key: block hash, value: A hash of the next block of the specified hash
10
+ agg_pubkey: 'a', # key: index, value: Activated block height | aggregated public key.
11
+ latest_agg_pubkey: 'g' # value: latest agg pubkey index.
10
12
  }
11
13
 
12
14
  class SPVChain
@@ -14,10 +16,14 @@ module Tapyrus
14
16
  attr_reader :db
15
17
  attr_reader :logger
16
18
 
17
- def initialize(db = Tapyrus::Store::DB::LevelDB.new)
19
+ # initialize spv chain
20
+ # @param[Tapyrus::Store::DB::LevelDB] db
21
+ # @param[Tapyrus::Block] genesis genesis block
22
+ def initialize(db = Tapyrus::Store::DB::LevelDB.new, genesis: nil)
23
+ raise ArgumentError, 'genesis block should be specified.' unless genesis
18
24
  @db = db # TODO multiple db switch
19
25
  @logger = Tapyrus::Logger.create(:debug)
20
- initialize_block
26
+ initialize_block(genesis)
21
27
  end
22
28
 
23
29
  # get latest block in the store.
@@ -45,9 +51,9 @@ module Tapyrus
45
51
  # @return [Tapyrus::Store::ChainEntry] appended block header entry.
46
52
  def append_header(header)
47
53
  logger.info("append header #{header.block_id}")
48
- raise "this header is invalid. #{header.block_hash}" unless header.valid?
49
54
  best_block = latest_block
50
55
  current_height = best_block.height
56
+ raise "this header is invalid. #{header.block_hash}" unless header.valid?(db.agg_pubkey_with_height(current_height + 1))
51
57
  if best_block.block_hash == header.prev_hash
52
58
  entry = Tapyrus::Store::ChainEntry.new(header, current_height + 1)
53
59
  db.save_entry(entry)
@@ -83,14 +89,26 @@ module Tapyrus
83
89
  time[time.size / 2]
84
90
  end
85
91
 
92
+ # Add aggregated public key.
93
+ # @param [Integer] active_height
94
+ # @param [String] agg_pubkey aggregated public key with hex format.
95
+ def add_agg_pubkey(active_height, agg_pubkey)
96
+ db.add_agg_pubkey(active_height, agg_pubkey)
97
+ end
98
+
99
+ # get aggregated public key keys.
100
+ # @return [Array[Array(height, agg_pubkey)]] the list of public keys.
101
+ def agg_pubkeys
102
+ db.agg_pubkeys
103
+ end
104
+
86
105
  private
87
106
 
88
107
  # if database is empty, put genesis block.
89
- def initialize_block
108
+ # @param [Tapyrus::Block] genesis genesis block
109
+ def initialize_block(genesis)
90
110
  unless latest_block
91
- block = Tapyrus.chain_params.genesis_block
92
- genesis = ChainEntry.new(block.header, 0)
93
- db.save_entry(genesis)
111
+ db.save_entry(ChainEntry.new(genesis.header, 0))
94
112
  end
95
113
  end
96
114
 
@@ -1,3 +1,3 @@
1
1
  module Tapyrus
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tapyrus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-15 00:00:00.000000000 Z
11
+ date: 2020-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ecdsa
@@ -328,7 +328,9 @@ files:
328
328
  - lib/tapyrus/chainparams/dev.yml
329
329
  - lib/tapyrus/chainparams/prod.yml
330
330
  - lib/tapyrus/constants.rb
331
+ - lib/tapyrus/errors.rb
331
332
  - lib/tapyrus/ext.rb
333
+ - lib/tapyrus/ext/ecdsa.rb
332
334
  - lib/tapyrus/ext/json_parser.rb
333
335
  - lib/tapyrus/ext_key.rb
334
336
  - lib/tapyrus/key.rb