tapyrus 0.2.0 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- 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 +15 -36
- 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 +2 -2
- data/lib/tapyrus/script/color.rb +2 -2
- data/lib/tapyrus/script/script.rb +30 -0
- data/lib/tapyrus/script/tx_checker.rb +7 -3
- data/lib/tapyrus/secp256k1/native.rb +93 -20
- data/lib/tapyrus/secp256k1/ruby.rb +62 -27
- 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
- data/tapyrusrb.gemspec +2 -2
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6eacc6d47ecb8c8f955e1713d0245b95185857a313189e232102a24e9dcf43c6
|
4
|
+
data.tar.gz: a961c657873aa82e7f917f55c8b00ea80175b9eb2eb363d72c3c1621f7bee9dc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 32adab54eb182f5f24536a0b48a89c05756901ba623b054916d7ba5ccc3e034f70147e578917bf847eabdb93070db3b7c628fe3ecc60ebc810ad4603497ec367
|
7
|
+
data.tar.gz: e7c802d4cd4081f0dcbf10c317551c47c7ab90ae764e341dfb55e7f32f91bfb28a0680d1ef915ae322ed15ca1524824df690c9e32e7d29fe27df8e080164f5bf
|
data/README.md
CHANGED
@@ -12,7 +12,7 @@ Tapyrusrb supports following feature:
|
|
12
12
|
* Tapyrus script interpreter
|
13
13
|
* De/serialization of Tapyrus protocol network messages
|
14
14
|
* De/serialization of blocks and transactions
|
15
|
-
* Key generation and verification for ECDSA
|
15
|
+
* Key generation and verification for Schnorr and ECDSA (including [BIP-32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) and [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) supports).
|
16
16
|
* ECDSA signature(RFC6979 -Deterministic ECDSA, LOW-S, LOW-R support)
|
17
17
|
* [WIP] SPV node
|
18
18
|
* [WIP] 0ff-chain protocol
|
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]
|
@@ -95,10 +96,13 @@ module Tapyrus
|
|
95
96
|
# @return [String] signature data with binary format
|
96
97
|
def sign(data, low_r = true, extra_entropy = nil, algo: :ecdsa)
|
97
98
|
raise ArgumentError, "Unsupported algorithm has been specified." unless SIG_ALGO.include?(algo)
|
98
|
-
|
99
|
+
case algo
|
100
|
+
when :ecdsa
|
99
101
|
sign_ecdsa(data, low_r, extra_entropy)
|
102
|
+
when :schnorr
|
103
|
+
secp256k1_module.sign_data(data, priv_key, extra_entropy, algo: algo)
|
100
104
|
else
|
101
|
-
|
105
|
+
false
|
102
106
|
end
|
103
107
|
end
|
104
108
|
|
@@ -111,11 +115,8 @@ module Tapyrus
|
|
111
115
|
return false unless valid_pubkey?
|
112
116
|
begin
|
113
117
|
raise ArgumentError, "Unsupported algorithm has been specified." unless SIG_ALGO.include?(algo)
|
114
|
-
if algo == :ecdsa
|
115
|
-
|
116
|
-
else
|
117
|
-
verify_schnorr_sig(sig, origin)
|
118
|
-
end
|
118
|
+
sig = ecdsa_signature_parse_der_lax(sig) if algo == :ecdsa
|
119
|
+
secp256k1_module.verify_sig(origin, sig, pubkey, algo: algo)
|
119
120
|
rescue Exception
|
120
121
|
false
|
121
122
|
end
|
@@ -223,14 +224,8 @@ 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
|
-
|
228
|
-
begin
|
229
|
-
point = ECDSA::Format::PointOctetString.decode(pubkey.htb, ECDSA::Group::Secp256k1)
|
230
|
-
ECDSA::Group::Secp256k1.valid_public_key?(point)
|
231
|
-
rescue ECDSA::Format::DecodeError
|
232
|
-
false
|
233
|
-
end
|
227
|
+
def fully_valid_pubkey?(allow_hybrid = false)
|
228
|
+
valid_pubkey? && secp256k1_module.parse_ec_pubkey?(pubkey, allow_hybrid)
|
234
229
|
end
|
235
230
|
|
236
231
|
private
|
@@ -291,34 +286,18 @@ module Tapyrus
|
|
291
286
|
|
292
287
|
# generate ecdsa signature
|
293
288
|
def sign_ecdsa(data, low_r, extra_entropy)
|
294
|
-
sig = secp256k1_module.sign_data(data, priv_key, extra_entropy)
|
289
|
+
sig = secp256k1_module.sign_data(data, priv_key, extra_entropy, algo: :ecdsa)
|
295
290
|
if low_r && !sig_has_low_r?(sig)
|
296
291
|
counter = 1
|
297
292
|
until sig_has_low_r?(sig)
|
298
293
|
extra_entropy = [counter].pack('I*').bth.ljust(64, '0').htb
|
299
|
-
sig = secp256k1_module.sign_data(data, priv_key, extra_entropy)
|
294
|
+
sig = secp256k1_module.sign_data(data, priv_key, extra_entropy, algo: :ecdsa)
|
300
295
|
counter += 1
|
301
296
|
end
|
302
297
|
end
|
303
298
|
sig
|
304
299
|
end
|
305
300
|
|
306
|
-
# generate schnorr signature
|
307
|
-
def sign_schnorr(msg)
|
308
|
-
Schnorr.sign(msg, priv_key.to_i(16)).encode
|
309
|
-
end
|
310
|
-
|
311
|
-
# verify ecdsa signature
|
312
|
-
def verify_ecdsa_sig(sig, message)
|
313
|
-
sig = ecdsa_signature_parse_der_lax(sig)
|
314
|
-
secp256k1_module.verify_sig(message, sig, pubkey)
|
315
|
-
end
|
316
|
-
|
317
|
-
# verify schnorr signature
|
318
|
-
def verify_schnorr_sig(sig, message)
|
319
|
-
Schnorr.valid_sig?(message, sig, pubkey.htb)
|
320
|
-
end
|
321
|
-
|
322
301
|
end
|
323
302
|
|
324
303
|
end
|
@@ -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
|
}
|
@@ -57,14 +57,14 @@ module Tapyrus
|
|
57
57
|
uri = URI.parse(server_url)
|
58
58
|
http = Net::HTTP.new(uri.hostname, uri.port)
|
59
59
|
http.use_ssl = uri.scheme === "https"
|
60
|
-
request = Net::HTTP::Post.new('/')
|
60
|
+
request = Net::HTTP::Post.new(uri.path.empty? ? '/' : uri.path)
|
61
61
|
request.basic_auth(uri.user, uri.password)
|
62
62
|
request.content_type = 'application/json'
|
63
63
|
request.body = data.to_json
|
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
|
|
data/lib/tapyrus/script/color.rb
CHANGED
@@ -53,11 +53,11 @@ module Tapyrus
|
|
53
53
|
|
54
54
|
module ColoredOutput
|
55
55
|
def colored?
|
56
|
-
script_pubkey.
|
56
|
+
script_pubkey.colored?
|
57
57
|
end
|
58
58
|
|
59
59
|
def color_id
|
60
|
-
@color_id ||=
|
60
|
+
@color_id ||= script_pubkey.color_id
|
61
61
|
end
|
62
62
|
|
63
63
|
def reissuable?
|
@@ -75,6 +75,18 @@ module Tapyrus
|
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
|
+
# Remove color identifier from cp2pkh or cp2sh
|
79
|
+
# @param [ColorIdentifier] color identifier
|
80
|
+
# @return [Script] P2PKH or P2SH script
|
81
|
+
# @raise [RuntimeError] if script is neither cp2pkh nor cp2sh
|
82
|
+
def remove_color
|
83
|
+
raise RuntimeError, 'Only cp2pkh and cp2sh can remove color' unless cp2pkh? or cp2sh?
|
84
|
+
|
85
|
+
Tapyrus::Script.new.tap do |s|
|
86
|
+
s.chunks = self.chunks[2..-1]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
78
90
|
def get_multisig_pubkeys
|
79
91
|
num = Tapyrus::Opcodes.opcode_to_small_int(chunks[-2].bth.to_i(16))
|
80
92
|
(1..num).map{ |i| chunks[i].pushed_data }
|
@@ -214,6 +226,8 @@ module Tapyrus
|
|
214
226
|
(chunks.size == 1 || chunks[1].opcode <= OP_16)
|
215
227
|
end
|
216
228
|
|
229
|
+
# Return whether this script is a CP2PKH format script or not.
|
230
|
+
# @return [Boolean] true if this script is cp2pkh, otherwise false.
|
217
231
|
def cp2pkh?
|
218
232
|
return false unless chunks.size == 7
|
219
233
|
return false unless chunks[0].bytesize == 34
|
@@ -223,6 +237,8 @@ module Tapyrus
|
|
223
237
|
(chunks[2..3]+ chunks[5..6]).map(&:ord) && chunks[4].bytesize == 21
|
224
238
|
end
|
225
239
|
|
240
|
+
# Return whether this script is a CP2SH format script or not.
|
241
|
+
# @return [Boolean] true if this script is cp2pkh, otherwise false.
|
226
242
|
def cp2sh?
|
227
243
|
return false unless chunks.size == 5
|
228
244
|
return false unless chunks[0].bytesize == 34
|
@@ -230,6 +246,20 @@ module Tapyrus
|
|
230
246
|
return false unless chunks[1].ord == OP_COLOR
|
231
247
|
OP_HASH160 == chunks[2].ord && OP_EQUAL == chunks[4].ord && chunks[3].bytesize == 21
|
232
248
|
end
|
249
|
+
|
250
|
+
# Return whether this script represents colored coin.
|
251
|
+
# @return [Boolean] true if this script is colored, otherwise false.
|
252
|
+
def colored?
|
253
|
+
cp2pkh? || cp2sh?
|
254
|
+
end
|
255
|
+
|
256
|
+
# Return color identifier for this script.
|
257
|
+
# @return [ColorIdentifer] color identifier for this script if this script is colored. return nil if this script is not colored.
|
258
|
+
def color_id
|
259
|
+
return nil unless colored?
|
260
|
+
|
261
|
+
Tapyrus::Color::ColorIdentifier.parse_from_payload(chunks[0].pushed_data)
|
262
|
+
end
|
233
263
|
|
234
264
|
def op_return_data
|
235
265
|
return nil unless op_return?
|
@@ -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)
|
@@ -4,8 +4,8 @@
|
|
4
4
|
module Tapyrus
|
5
5
|
module Secp256k1
|
6
6
|
|
7
|
-
# binding for secp256k1 (https://github.com/
|
8
|
-
# tag: v0.
|
7
|
+
# binding for secp256k1 (https://github.com/chaintope/tapyrus-core/tree/v0.4.0/src/secp256k1)
|
8
|
+
# tag: v0.4.0
|
9
9
|
# this is not included by default, to enable set shared object path to ENV['SECP256K1_LIB_PATH']
|
10
10
|
# for linux, ENV['SECP256K1_LIB_PATH'] = '/usr/local/lib/libsecp256k1.so'
|
11
11
|
# for mac,
|
@@ -50,6 +50,8 @@ module Tapyrus
|
|
50
50
|
attach_function(:secp256k1_ecdsa_signature_parse_der, [:pointer, :pointer, :pointer, :size_t], :int)
|
51
51
|
attach_function(:secp256k1_ecdsa_signature_normalize, [:pointer, :pointer, :pointer], :int)
|
52
52
|
attach_function(:secp256k1_ecdsa_verify, [:pointer, :pointer, :pointer, :pointer], :int)
|
53
|
+
attach_function(:secp256k1_schnorr_sign, [:pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int)
|
54
|
+
attach_function(:secp256k1_schnorr_verify, [:pointer, :pointer, :pointer, :pointer], :int)
|
53
55
|
end
|
54
56
|
|
55
57
|
def with_context(flags: (SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN))
|
@@ -100,7 +102,68 @@ module Tapyrus
|
|
100
102
|
# @param [String] privkey a private key using sign
|
101
103
|
# @param [String] extra_entropy a extra entropy for rfc6979
|
102
104
|
# @return [String] signature data with binary format
|
103
|
-
def sign_data(data, privkey, extra_entropy)
|
105
|
+
def sign_data(data, privkey, extra_entropy, algo: :ecdsa)
|
106
|
+
case algo
|
107
|
+
when :ecdsa
|
108
|
+
sign_ecdsa(data, privkey, extra_entropy)
|
109
|
+
when :schnorr
|
110
|
+
sign_schnorr(data, privkey)
|
111
|
+
else
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# verify signature.
|
117
|
+
# @param[String] data a data.
|
118
|
+
# @param [String] sig signature data with binary format
|
119
|
+
# @param [String] pub_key a public key using verify.
|
120
|
+
def verify_sig(data, sig, pub_key, algo: :ecdsa)
|
121
|
+
case algo
|
122
|
+
when :ecdsa
|
123
|
+
verify_ecdsa(data, sig, pub_key)
|
124
|
+
when :schnorr
|
125
|
+
verify_schnorr(data, sig, pub_key)
|
126
|
+
else
|
127
|
+
false
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# # validate whether this is a valid public key (more expensive than IsValid())
|
132
|
+
# @param [String] pub_key public key with hex format.
|
133
|
+
# @param [Boolean] allow_hybrid whether support hybrid public key.
|
134
|
+
# @return [Boolean] If valid public key return true, otherwise false.
|
135
|
+
def parse_ec_pubkey?(pub_key, allow_hybrid = false)
|
136
|
+
pub_key = pub_key.htb
|
137
|
+
return false if !allow_hybrid && ![0x02, 0x03, 0x04].include?(pub_key[0].ord)
|
138
|
+
with_context do |context|
|
139
|
+
pubkey = FFI::MemoryPointer.new(:uchar, pub_key.bytesize).put_bytes(0, pub_key)
|
140
|
+
internal_pubkey = FFI::MemoryPointer.new(:uchar, 64)
|
141
|
+
result = secp256k1_ec_pubkey_parse(context, internal_pubkey, pubkey, pub_key.bytesize)
|
142
|
+
result == 1
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def generate_pubkey_in_context(context, privkey, compressed: true)
|
149
|
+
internal_pubkey = FFI::MemoryPointer.new(:uchar, 64)
|
150
|
+
result = secp256k1_ec_pubkey_create(context, internal_pubkey, privkey.htb)
|
151
|
+
raise 'error creating pubkey' unless result
|
152
|
+
|
153
|
+
pubkey = FFI::MemoryPointer.new(:uchar, 65)
|
154
|
+
pubkey_len = FFI::MemoryPointer.new(:uint64)
|
155
|
+
result = if compressed
|
156
|
+
pubkey_len.put_uint64(0, 33)
|
157
|
+
secp256k1_ec_pubkey_serialize(context, pubkey, pubkey_len, internal_pubkey, SECP256K1_EC_COMPRESSED)
|
158
|
+
else
|
159
|
+
pubkey_len.put_uint64(0, 65)
|
160
|
+
secp256k1_ec_pubkey_serialize(context, pubkey, pubkey_len, internal_pubkey, SECP256K1_EC_UNCOMPRESSED)
|
161
|
+
end
|
162
|
+
raise 'error serialize pubkey' unless result || pubkey_len.read_uint64 > 0
|
163
|
+
pubkey.read_string(pubkey_len.read_uint64).bth
|
164
|
+
end
|
165
|
+
|
166
|
+
def sign_ecdsa(data, privkey, extra_entropy)
|
104
167
|
with_context do |context|
|
105
168
|
secret = FFI::MemoryPointer.new(:uchar, privkey.htb.bytesize).put_bytes(0, privkey.htb)
|
106
169
|
raise 'priv_key invalid' unless secp256k1_ec_seckey_verify(context, secret)
|
@@ -126,7 +189,19 @@ module Tapyrus
|
|
126
189
|
end
|
127
190
|
end
|
128
191
|
|
129
|
-
def
|
192
|
+
def sign_schnorr(data, privkey)
|
193
|
+
with_context do |context|
|
194
|
+
secret = FFI::MemoryPointer.new(:uchar, privkey.htb.bytesize).put_bytes(0, privkey.htb)
|
195
|
+
raise 'priv_key invalid' unless secp256k1_ec_seckey_verify(context, secret)
|
196
|
+
|
197
|
+
signature = FFI::MemoryPointer.new(:uchar, 64)
|
198
|
+
msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, data)
|
199
|
+
raise 'Failed to generate schnorr signature.' unless secp256k1_schnorr_sign(context, signature, msg32, secret, nil, nil) == 1
|
200
|
+
signature.read_string(64)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def verify_ecdsa(data, sig, pub_key)
|
130
205
|
with_context do |context|
|
131
206
|
return false if data.bytesize == 0
|
132
207
|
|
@@ -150,25 +225,23 @@ module Tapyrus
|
|
150
225
|
end
|
151
226
|
end
|
152
227
|
|
153
|
-
|
228
|
+
def verify_schnorr(data, sig, pub_key)
|
229
|
+
with_context do |context|
|
230
|
+
return false if data.bytesize == 0
|
231
|
+
pubkey = FFI::MemoryPointer.new(:uchar, pub_key.htb.bytesize).put_bytes(0, pub_key.htb)
|
232
|
+
internal_pubkey = FFI::MemoryPointer.new(:uchar, 64)
|
233
|
+
result = secp256k1_ec_pubkey_parse(context, internal_pubkey, pubkey, pubkey.size)
|
234
|
+
return false unless result
|
154
235
|
|
155
|
-
|
156
|
-
internal_pubkey = FFI::MemoryPointer.new(:uchar, 64)
|
157
|
-
result = secp256k1_ec_pubkey_create(context, internal_pubkey, privkey.htb)
|
158
|
-
raise 'error creating pubkey' unless result
|
236
|
+
signature = FFI::MemoryPointer.new(:uchar, sig.bytesize).put_bytes(0, sig)
|
159
237
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
else
|
166
|
-
pubkey_len.put_uint64(0, 65)
|
167
|
-
secp256k1_ec_pubkey_serialize(context, pubkey, pubkey_len, internal_pubkey, SECP256K1_EC_UNCOMPRESSED)
|
168
|
-
end
|
169
|
-
raise 'error serialize pubkey' unless result || pubkey_len.read_uint64 > 0
|
170
|
-
pubkey.read_string(pubkey_len.read_uint64).bth
|
238
|
+
msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, data)
|
239
|
+
result = secp256k1_schnorr_verify(context, signature, msg32, internal_pubkey)
|
240
|
+
|
241
|
+
result == 1
|
242
|
+
end
|
171
243
|
end
|
244
|
+
|
172
245
|
end
|
173
246
|
end
|
174
247
|
end
|
@@ -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,14 +24,71 @@ 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.
|
31
31
|
# @param [String] data a data to be signed with binary format
|
32
32
|
# @param [String] privkey a private key using sign
|
33
33
|
# @return [String] signature data with binary format
|
34
|
-
def sign_data(data, privkey, extra_entropy)
|
34
|
+
def sign_data(data, privkey, extra_entropy, algo: :ecdsa)
|
35
|
+
case algo
|
36
|
+
when :ecdsa
|
37
|
+
sign_ecdsa(data, privkey, extra_entropy)
|
38
|
+
when :schnorr
|
39
|
+
Schnorr.sign(data, privkey.to_i(16)).encode
|
40
|
+
else
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# verify signature using public key
|
46
|
+
# @param [String] digest a SHA-256 message digest with binary format
|
47
|
+
# @param [String] sig a signature for +data+ with binary format
|
48
|
+
# @param [String] pubkey a public key corresponding to the private key used for sign
|
49
|
+
# @return [Boolean] verify result
|
50
|
+
def verify_sig(digest, sig, pubkey, algo: :ecdsa)
|
51
|
+
case algo
|
52
|
+
when :ecdsa
|
53
|
+
verify_ecdsa(digest, sig, pubkey)
|
54
|
+
when :schnorr
|
55
|
+
Schnorr.valid_sig?(digest, sig, pubkey.htb)
|
56
|
+
else
|
57
|
+
false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
alias :valid_sig? :verify_sig
|
62
|
+
|
63
|
+
module_function :valid_sig?
|
64
|
+
|
65
|
+
# validate whether this is a valid public key (more expensive than IsValid())
|
66
|
+
# @param [String] pubkey public key with hex format.
|
67
|
+
# @param [Boolean] allow_hybrid whether support hybrid public key.
|
68
|
+
# @return [Boolean] If valid public key return true, otherwise false.
|
69
|
+
def parse_ec_pubkey?(pubkey, allow_hybrid = false)
|
70
|
+
begin
|
71
|
+
point = ECDSA::Format::PointOctetString.decode(pubkey.htb, ECDSA::Group::Secp256k1, allow_hybrid: allow_hybrid)
|
72
|
+
ECDSA::Group::Secp256k1.valid_public_key?(point)
|
73
|
+
rescue ECDSA::Format::DecodeError
|
74
|
+
false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# if +pubkey+ is hybrid public key format, it convert uncompressed format.
|
79
|
+
# https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2012-June/001578.html
|
80
|
+
def repack_pubkey(pubkey)
|
81
|
+
p = pubkey.htb
|
82
|
+
case p[0]
|
83
|
+
when "\x06", "\x07"
|
84
|
+
p[0] = "\x04"
|
85
|
+
p
|
86
|
+
else
|
87
|
+
pubkey.htb
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def sign_ecdsa(data, privkey, extra_entropy)
|
35
92
|
privkey = privkey.htb
|
36
93
|
private_key = ECDSA::Format::IntegerOctetString.decode(privkey)
|
37
94
|
extra_entropy ||= ''
|
@@ -59,12 +116,7 @@ module Tapyrus
|
|
59
116
|
signature
|
60
117
|
end
|
61
118
|
|
62
|
-
|
63
|
-
# @param [String] digest a SHA-256 message digest with binary format
|
64
|
-
# @param [String] sig a signature for +data+ with binary format
|
65
|
-
# @param [String] pubkey a public key corresponding to the private key used for sign
|
66
|
-
# @return [Boolean] verify result
|
67
|
-
def verify_sig(digest, sig, pubkey)
|
119
|
+
def verify_ecdsa(digest, sig, pubkey)
|
68
120
|
begin
|
69
121
|
k = ECDSA::Format::PointOctetString.decode(repack_pubkey(pubkey), GROUP)
|
70
122
|
signature = ECDSA::Format::SignatureDerString.decode(sig)
|
@@ -74,23 +126,6 @@ module Tapyrus
|
|
74
126
|
end
|
75
127
|
end
|
76
128
|
|
77
|
-
alias :valid_sig? :verify_sig
|
78
|
-
|
79
|
-
module_function :valid_sig?
|
80
|
-
|
81
|
-
# if +pubkey+ is hybrid public key format, it convert uncompressed format.
|
82
|
-
# https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2012-June/001578.html
|
83
|
-
def repack_pubkey(pubkey)
|
84
|
-
p = pubkey.htb
|
85
|
-
case p[0]
|
86
|
-
when "\x06", "\x07"
|
87
|
-
p[0] = "\x04"
|
88
|
-
p
|
89
|
-
else
|
90
|
-
pubkey.htb
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
129
|
end
|
95
130
|
|
96
131
|
end
|
@@ -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
data/tapyrusrb.gemspec
CHANGED
@@ -10,8 +10,8 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.authors = ["azuchi"]
|
11
11
|
spec.email = ["azuchi@chaintope.com"]
|
12
12
|
|
13
|
-
spec.summary = %q{
|
14
|
-
spec.description = %q{
|
13
|
+
spec.summary = %q{The implementation of Tapyrus Protocol for Ruby.}
|
14
|
+
spec.description = %q{The implementation of Tapyrus Protocol for Ruby.}
|
15
15
|
spec.homepage = 'https://github.com/chaintope/tapyrusrb'
|
16
16
|
spec.license = "MIT"
|
17
17
|
|
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.5
|
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-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ecdsa
|
@@ -290,7 +290,7 @@ dependencies:
|
|
290
290
|
- - "~>"
|
291
291
|
- !ruby/object:Gem::Version
|
292
292
|
version: '3.0'
|
293
|
-
description:
|
293
|
+
description: The implementation of Tapyrus Protocol for Ruby.
|
294
294
|
email:
|
295
295
|
- azuchi@chaintope.com
|
296
296
|
executables:
|
@@ -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
|
@@ -450,5 +452,5 @@ requirements: []
|
|
450
452
|
rubygems_version: 3.0.3
|
451
453
|
signing_key:
|
452
454
|
specification_version: 4
|
453
|
-
summary:
|
455
|
+
summary: The implementation of Tapyrus Protocol for Ruby.
|
454
456
|
test_files: []
|