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 +4 -4
- data/lib/schnorr.rb +1 -1
- data/lib/tapyrus.rb +2 -7
- data/lib/tapyrus/block.rb +3 -7
- data/lib/tapyrus/block_header.rb +66 -38
- data/lib/tapyrus/chain_params.rb +0 -10
- data/lib/tapyrus/chainparams/dev.yml +0 -10
- data/lib/tapyrus/chainparams/prod.yml +0 -9
- data/lib/tapyrus/errors.rb +17 -0
- data/lib/tapyrus/ext/ecdsa.rb +39 -0
- data/lib/tapyrus/ext_key.rb +39 -15
- data/lib/tapyrus/key.rb +6 -5
- data/lib/tapyrus/message/block.rb +1 -1
- data/lib/tapyrus/message/header_and_short_ids.rb +1 -1
- data/lib/tapyrus/message/headers.rb +1 -1
- data/lib/tapyrus/message/merkle_block.rb +1 -1
- data/lib/tapyrus/rpc/request_handler.rb +5 -4
- data/lib/tapyrus/rpc/tapyrus_core_client.rb +1 -1
- data/lib/tapyrus/script/tx_checker.rb +7 -3
- data/lib/tapyrus/secp256k1/ruby.rb +3 -3
- data/lib/tapyrus/store/chain_entry.rb +2 -2
- data/lib/tapyrus/store/db/level_db.rb +58 -0
- data/lib/tapyrus/store/spv_chain.rb +29 -11
- data/lib/tapyrus/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a1abedf80b636e2bfd80db4617ca9f92e703148f07e59c08b14860c13ac57689
|
4
|
+
data.tar.gz: 21b478051eb01354e410475e15761f24773fe164fcd9abf3307acb231b5c8ae3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61c8cc2439074666abffc6ff85ffaa40811e8e0992edb19846876dabf9a300f4822af522a6f621271f310a9b1ea3f9429f30e86452d29b0f2216052568a72a6a
|
7
|
+
data.tar.gz: f3845015c030c7ccf915700eb45ac2f743c3f0c19dbe077c72c4afe19327252d1f3a64ffb29c1f5790997c8f6f2317930ab5ae99a894687a86f6381154c5429d
|
data/lib/schnorr.rb
CHANGED
@@ -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=
|
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
|
|
data/lib/tapyrus.rb
CHANGED
@@ -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
|
data/lib/tapyrus/block.rb
CHANGED
@@ -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
|
-
#
|
36
|
+
# @return [Integer] block height.
|
36
37
|
def height
|
37
|
-
|
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
|
data/lib/tapyrus/block_header.rb
CHANGED
@@ -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 :
|
11
|
-
attr_accessor :
|
12
|
-
attr_accessor :
|
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,
|
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
|
-
@
|
20
|
-
@
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
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
|
-
#
|
70
|
-
# @
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
82
|
-
|
83
|
-
def
|
84
|
-
|
109
|
+
# get bytesize.
|
110
|
+
# @return [Integer] bytesize.
|
111
|
+
def size
|
112
|
+
to_payload.bytesize
|
85
113
|
end
|
86
114
|
|
87
115
|
end
|
data/lib/tapyrus/chain_params.rb
CHANGED
@@ -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
|
data/lib/tapyrus/ext_key.rb
CHANGED
@@ -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('
|
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
|
-
|
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
|
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(
|
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
|
-
|
241
|
-
|
242
|
-
|
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 =
|
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
|
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(
|
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.
|
data/lib/tapyrus/key.rb
CHANGED
@@ -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,
|
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,
|
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
|
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
|
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
|
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
|
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
|
-
|
40
|
-
|
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'].
|
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
|
-
|
40
|
-
|
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 =
|
16
|
-
[privkey.bth, pubkey
|
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
|
-
|
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
|
-
|
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
|
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',
|
7
|
-
height: 'h',
|
8
|
-
best: 'B',
|
9
|
-
next: 'n'
|
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
|
-
|
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
|
-
|
108
|
+
# @param [Tapyrus::Block] genesis genesis block
|
109
|
+
def initialize_block(genesis)
|
90
110
|
unless latest_block
|
91
|
-
|
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
|
|
data/lib/tapyrus/version.rb
CHANGED
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.
|
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-
|
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
|