tapyrus 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +2 -2
  3. data/.ruby-version +1 -1
  4. data/README.md +43 -2
  5. data/lib/tapyrus/ext_key.rb +8 -4
  6. data/lib/tapyrus/key.rb +1 -1
  7. data/lib/tapyrus/key_path.rb +17 -13
  8. data/lib/tapyrus/message/fee_filter.rb +2 -2
  9. data/lib/tapyrus/message/header_and_short_ids.rb +4 -4
  10. data/lib/tapyrus/message/inventory.rb +1 -1
  11. data/lib/tapyrus/message/network_addr.rb +2 -2
  12. data/lib/tapyrus/message/ping.rb +2 -2
  13. data/lib/tapyrus/message/pong.rb +2 -2
  14. data/lib/tapyrus/message/send_cmpct.rb +2 -2
  15. data/lib/tapyrus/message/version.rb +3 -3
  16. data/lib/tapyrus/mnemonic.rb +9 -4
  17. data/lib/tapyrus/pstt/input.rb +422 -0
  18. data/lib/tapyrus/pstt/key_origin_info.rb +63 -0
  19. data/lib/tapyrus/pstt/output.rb +129 -0
  20. data/lib/tapyrus/pstt/proprietary.rb +54 -0
  21. data/lib/tapyrus/pstt/tx.rb +658 -0
  22. data/lib/tapyrus/pstt.rb +335 -0
  23. data/lib/tapyrus/rpc.rb +0 -2
  24. data/lib/tapyrus/script/script.rb +14 -4
  25. data/lib/tapyrus/secp256k1/rfc6979.rb +6 -3
  26. data/lib/tapyrus/tip0020.rb +341 -0
  27. data/lib/tapyrus/tx.rb +6 -2
  28. data/lib/tapyrus/tx_out.rb +2 -2
  29. data/lib/tapyrus/util.rb +3 -3
  30. data/lib/tapyrus/version.rb +1 -1
  31. data/lib/tapyrus/wallet/account.rb +2 -2
  32. data/lib/tapyrus/wallet/db.rb +3 -3
  33. data/lib/tapyrus/wallet/master_key.rb +2 -2
  34. data/lib/tapyrus.rb +3 -3
  35. data/tapyrusrb.gemspec +0 -3
  36. metadata +10 -61
  37. data/exe/tapyrusrb-cli +0 -5
  38. data/exe/tapyrusrbd +0 -42
  39. data/lib/tapyrus/network/connection.rb +0 -70
  40. data/lib/tapyrus/network/message_handler.rb +0 -244
  41. data/lib/tapyrus/network/peer.rb +0 -210
  42. data/lib/tapyrus/network/peer_discovery.rb +0 -43
  43. data/lib/tapyrus/network/pool.rb +0 -135
  44. data/lib/tapyrus/network.rb +0 -11
  45. data/lib/tapyrus/node/cli.rb +0 -114
  46. data/lib/tapyrus/node/configuration.rb +0 -36
  47. data/lib/tapyrus/node/spv.rb +0 -78
  48. data/lib/tapyrus/node.rb +0 -7
  49. data/lib/tapyrus/rpc/http_server.rb +0 -64
  50. data/lib/tapyrus/rpc/request_handler.rb +0 -146
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2b7edea1facc6228c16aad20bb3e276ed43bb11f97de6bf0c4075cdaa02ccd0
4
- data.tar.gz: 95b04f8b54cff7056b054349bc4ed86eed4a580479118fcfcdd872461f1a99d6
3
+ metadata.gz: 9ceb5c0fcc47319d15e66a21760c345cb1ee029a71f1313046f1676e883972a5
4
+ data.tar.gz: 13a2463ef2432f904acef3b9880a275d0e174485086a469d9a9177dcebf9f200
5
5
  SHA512:
6
- metadata.gz: 47a70989491951817c29e1b984f1ff3a873cb94722bbcab149d86c841647c69593ad087d72217742b152eb3cb251c29e8ce43ec8730027b1d3ba7cf3482c4332
7
- data.tar.gz: f2703d586a1751da3c7145cbf4d2e14a4bcc498f43911bce9518846a3d299a5d4d1cda0115a6bd6bdf6ac47fe3766ca5110b0366be47b68331a59a2f62470a7a
6
+ metadata.gz: 8a89f3d1a2bca680384a69dd4793d43a4c102e67ae14e69e4d88205ef46c343e8efe015eaf1b586c17e4b1163814628425282cb3911b506397b28ee49ac66ac7
7
+ data.tar.gz: d819136137c7571173e98576ce21b5b08b1c5779269baddbdd49bcd4bfcffc51076e5ecee0c27ddfba56c3ca34f8c70e68e1cc3f76165fe4bdea21a6060ad0d3
@@ -18,11 +18,11 @@ jobs:
18
18
  runs-on: ubuntu-latest
19
19
  strategy:
20
20
  matrix:
21
- ruby-version: ["3.0", "3.1", "3.2", "3.3", "3.4"]
21
+ ruby-version: ["3.2", "3.3", "3.4", "4.0"]
22
22
 
23
23
  steps:
24
24
  - run: sudo apt install libleveldb-dev
25
- - uses: actions/checkout@v4
25
+ - uses: actions/checkout@v5
26
26
  - name: Set up Ruby
27
27
  # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
28
28
  # change this to (see https://github.com/ruby/setup-ruby#versioning):
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.1
1
+ ruby-4.0.0
data/README.md CHANGED
@@ -13,8 +13,8 @@ Tapyrusrb supports following feature:
13
13
  - De/serialization of blocks and transactions
14
14
  - 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).
15
15
  - ECDSA signature(RFC6979 -Deterministic ECDSA, LOW-S, LOW-R support)
16
- - [WIP] SPV node
17
- - [WIP] 0ff-chain protocol
16
+ - Schnorr signature
17
+ - Partially Signed Tapyrus Transaction ([TIP-0174](https://github.com/chaintope/tips/blob/master/tip-0174.md))
18
18
 
19
19
  ## Requirements
20
20
 
@@ -80,6 +80,47 @@ Tapyrus.chain_params = :dev
80
80
 
81
81
  This parameter is described in https://github.com/chaintope/tapyrusrb/blob/master/lib/tapyrus/chainparams/dev.yml.
82
82
 
83
+ ### PSTT (Partially Signed Tapyrus Transaction)
84
+
85
+ `Tapyrus::PSTT` implements the format [TIP-0174](https://github.com/chaintope/tips/blob/master/tip-0174.md) defines.
86
+ Its methods follow the roles of the TIP: Creator, Constructor, Updater, Signer, Combiner, Input Finalizer and
87
+ Transaction Extractor.
88
+
89
+ ```ruby
90
+ # Creator
91
+ pstt =
92
+ Tapyrus::PSTT::Tx.new(
93
+ inputs: [Tapyrus::PSTT::Input.new(out_point: out_point, utxo: prev_tx)],
94
+ outputs: [Tapyrus::PSTT::Output.new(amount: 99_000, script_pubkey: script_pubkey)]
95
+ )
96
+
97
+ # Signer. The signature hash is computed from the fields of the PSTT.
98
+ pstt.sign(0, key) # ECDSA. Pass algo: :schnorr for a Schnorr signature.
99
+
100
+ # Input Finalizer and Transaction Extractor
101
+ pstt.finalize!
102
+ tx = pstt.extract_tx
103
+
104
+ # For text transport
105
+ Tapyrus::PSTT::Tx.from_base64(pstt.to_base64)
106
+ ```
107
+
108
+ A PSTT which other parties will extend is created with `tx_modifiable`, so that a Constructor can add inputs and
109
+ outputs afterwards. This is what a fee provider, which supplies the TPC that pays the fee of a Colored Coin
110
+ transaction, needs:
111
+
112
+ ```ruby
113
+ # The user leaves the inputs modifiable and signs with SIGHASH_ALL | SIGHASH_ANYONECANPAY.
114
+ pstt = Tapyrus::PSTT::Tx.new(tx_modifiable: Tapyrus::PSTT::TxModifiable::INPUTS, inputs: inputs, outputs: outputs)
115
+ pstt.sign(0, key, sighash_type: Tapyrus::SIGHASH_TYPE[:all] | Tapyrus::SIGHASH_TYPE[:anyonecanpay])
116
+
117
+ # The fee provider verifies the amounts per color, then adds its TPC input and signs with SIGHASH_ALL.
118
+ pstt.input_amounts # => { Tapyrus::Color::ColorIdentifier => amount }
119
+ pstt.fee
120
+ pstt.add_input(Tapyrus::PSTT::Input.new(out_point: fee_out_point, utxo: fee_tx))
121
+ pstt.sign(1, provider_key)
122
+ ```
123
+
83
124
  ## Contributing
84
125
 
85
126
  Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/tapyrusrb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
@@ -111,14 +111,16 @@ module Tapyrus
111
111
  child_priv = ECDSA::Format::IntegerOctetString.encode(child_priv, 32)
112
112
  new_key.key = Tapyrus::Key.new(priv_key: child_priv.bth, key_type: key_type)
113
113
  new_key.chain_code = l[32..-1]
114
- new_key.ver = version
114
+ # A depth 1 key derives its version bytes from the purpose, see #version.
115
+ new_key.ver = new_key.depth == 1 ? nil : version
115
116
  new_key
116
117
  end
117
118
 
118
119
  # get version bytes using serialization format
119
120
  def version
121
+ return ver if ver
120
122
  return ExtKey.version_from_purpose(number) if depth == 1
121
- ver ? ver : Tapyrus.chain_params.extended_privkey_version
123
+ Tapyrus.chain_params.extended_privkey_version
122
124
  end
123
125
 
124
126
  # get key type defined by BIP-178 using version.
@@ -282,14 +284,16 @@ module Tapyrus
282
284
  p2 = Tapyrus::Key.new(pubkey: pubkey, key_type: key_type).to_point
283
285
  new_key.pubkey = (p1 + p2).to_hex
284
286
  new_key.chain_code = l[32..-1]
285
- new_key.ver = version
287
+ # A depth 1 key derives its version bytes from the purpose, see #version.
288
+ new_key.ver = new_key.depth == 1 ? nil : version
286
289
  new_key
287
290
  end
288
291
 
289
292
  # get version bytes using serialization format
290
293
  def version
294
+ return ver if ver
291
295
  return ExtPubkey.version_from_purpose(number) if depth == 1
292
- ver ? ver : Tapyrus.chain_params.extended_pubkey_version
296
+ Tapyrus.chain_params.extended_pubkey_version
293
297
  end
294
298
 
295
299
  # get key type defined by BIP-178 using version.
data/lib/tapyrus/key.rb CHANGED
@@ -320,7 +320,7 @@ module Tapyrus
320
320
  if low_r && !sig_has_low_r?(sig)
321
321
  counter = 1
322
322
  until sig_has_low_r?(sig)
323
- extra_entropy = [counter].pack("I*").bth.ljust(64, "0").htb
323
+ extra_entropy = [counter].pack("V").bth.ljust(64, "0").htb
324
324
  sig = secp256k1_module.sign_data(data, priv_key, extra_entropy, algo: :ecdsa)
325
325
  counter += 1
326
326
  end
@@ -3,20 +3,24 @@ module Tapyrus
3
3
  # key path convert an array of derive number
4
4
  # @param [String] path_string
5
5
  # @return [Array[Integer]] key path numbers.
6
+ # @raise [ArgumentError] if the path is not a sequence of decimal indexes, each optionally
7
+ # followed by the hardened marker and each below Tapyrus::HARDENED_THRESHOLD.
6
8
  def parse_key_path(path_string)
7
- path_string
8
- .split("/")
9
- .map
10
- .with_index do |p, index|
11
- if index == 0
12
- raise ArgumentError.new("#{path_string} is invalid format.") unless p == "m"
13
- next
14
- end
15
- raise ArgumentError.new("#{path_string} is invalid format.") unless p.delete("'") =~ /^[0-9]+$/
16
- (p[-1] == "'" ? p.delete("'").to_i + Tapyrus::HARDENED_THRESHOLD : p.to_i)
17
- end[
18
- 1..-1
19
- ]
9
+ elements = path_string.split("/")
10
+ raise ArgumentError.new("#{path_string} is invalid format.") unless elements.first == "m"
11
+ elements[1..-1].map do |element|
12
+ hardened = element.end_with?("'")
13
+ # The index is validated before it is converted, since String#to_i stops at the first
14
+ # character which is not a digit and returns what it read so far. An element such as
15
+ # "1'2" would otherwise be taken for the index 1.
16
+ digits = hardened ? element[0..-2] : element
17
+ raise ArgumentError.new("#{path_string} is invalid format.") unless digits =~ /\A[0-9]+\z/
18
+ index = digits.to_i
19
+ # An index of Tapyrus::HARDENED_THRESHOLD or above is the hardened child of a smaller
20
+ # index, so it has no representation of its own in this notation.
21
+ raise ArgumentError.new("#{path_string} is invalid format.") if index >= Tapyrus::HARDENED_THRESHOLD
22
+ hardened ? index + Tapyrus::HARDENED_THRESHOLD : index
23
+ end
20
24
  end
21
25
 
22
26
  # key path numbers convert to path string.
@@ -13,11 +13,11 @@ module Tapyrus
13
13
  end
14
14
 
15
15
  def self.parse_from_payload(payload)
16
- new(payload.unpack("Q").first)
16
+ new(payload.unpack("Q<").first)
17
17
  end
18
18
 
19
19
  def to_payload
20
- [fee_rate].pack("Q")
20
+ [fee_rate].pack("Q<")
21
21
  end
22
22
  end
23
23
  end
@@ -15,13 +15,13 @@ module Tapyrus
15
15
  @nonce = nonce
16
16
  @short_ids = short_ids
17
17
  @prefilled_txn = prefilled_txn
18
- @siphash_key = Tapyrus.sha256(header.to_payload << [nonce].pack("q*"))[0...16]
18
+ @siphash_key = Tapyrus.sha256(header.to_payload << [nonce].pack("q<*"))[0...16]
19
19
  end
20
20
 
21
21
  def self.parse_from_payload(payload)
22
22
  buf = StringIO.new(payload)
23
23
  header = Tapyrus::BlockHeader.parse_from_payload(buf)
24
- nonce = buf.read(8).unpack("q*").first
24
+ nonce = buf.read(8).unpack("q<*").first
25
25
  short_ids_len = Tapyrus.unpack_var_int_from_io(buf)
26
26
  short_ids = short_ids_len.times.map { buf.read(6).reverse.bth.to_i(16) }
27
27
  prefilled_txn_len = Tapyrus.unpack_var_int_from_io(buf)
@@ -31,9 +31,9 @@ module Tapyrus
31
31
 
32
32
  def to_payload
33
33
  p = header.to_payload
34
- p << [nonce].pack("q*")
34
+ p << [nonce].pack("q<*")
35
35
  p << Tapyrus.pack_var_int(short_ids.size)
36
- p << short_ids.map { |id| sprintf("%12x", id).htb.reverse }.join
36
+ p << short_ids.map { |id| sprintf("%012x", id).htb.reverse }.join
37
37
  p << Tapyrus.pack_var_int(prefilled_txn.size)
38
38
  p << prefilled_txn.map(&:to_payload).join
39
39
  p
@@ -27,7 +27,7 @@ module Tapyrus
27
27
  # parse inventory payload
28
28
  def self.parse_from_payload(payload)
29
29
  raise Error, "invalid inventory size." if payload.bytesize != 36
30
- identifier = payload[0..4].unpack("V").first
30
+ identifier = payload[0..3].unpack("V").first
31
31
  hash = payload[4..-1].bth # internal byte order
32
32
  new(identifier, hash)
33
33
  end
@@ -35,7 +35,7 @@ module Tapyrus
35
35
  has_time = buf.size > 26
36
36
  addr = new(time: nil)
37
37
  addr.time = buf.read(4).unpack("V").first if has_time
38
- addr.services = buf.read(8).unpack("Q").first
38
+ addr.services = buf.read(8).unpack("Q<").first
39
39
  addr.ip_addr = IPAddr.new_ntoh(buf.read(16))
40
40
  addr.port = buf.read(2).unpack("n").first
41
41
  addr
@@ -57,7 +57,7 @@ module Tapyrus
57
57
  p = ""
58
58
  p << [time].pack("V") unless skip_time
59
59
  addr = ip_addr.ipv4? ? ip_addr.ipv4_mapped : ip_addr
60
- p << [services].pack("Q") << addr.hton << [port].pack("n")
60
+ p << [services].pack("Q<") << addr.hton << [port].pack("n")
61
61
  end
62
62
  end
63
63
  end
@@ -12,11 +12,11 @@ module Tapyrus
12
12
  end
13
13
 
14
14
  def self.parse_from_payload(payload)
15
- new(payload.unpack("Q").first)
15
+ new(payload.unpack("Q<").first)
16
16
  end
17
17
 
18
18
  def to_payload
19
- [nonce].pack("Q")
19
+ [nonce].pack("Q<")
20
20
  end
21
21
 
22
22
  def to_response
@@ -12,11 +12,11 @@ module Tapyrus
12
12
  end
13
13
 
14
14
  def self.parse_from_payload(payload)
15
- new(payload.unpack("Q").first)
15
+ new(payload.unpack("Q<").first)
16
16
  end
17
17
 
18
18
  def to_payload
19
- [nonce].pack("Q")
19
+ [nonce].pack("Q<")
20
20
  end
21
21
  end
22
22
  end
@@ -21,12 +21,12 @@ module Tapyrus
21
21
  def self.parse_from_payload(payload)
22
22
  buf = StringIO.new(payload)
23
23
  mode = buf.read(1).unpack("c").first
24
- version = buf.read(8).unpack("Q").first
24
+ version = buf.read(8).unpack("Q<").first
25
25
  new(mode, version)
26
26
  end
27
27
 
28
28
  def to_payload
29
- [mode, version].pack("cQ")
29
+ [mode, version].pack("cQ<")
30
30
  end
31
31
 
32
32
  def high?
@@ -30,7 +30,7 @@ module Tapyrus
30
30
  end
31
31
 
32
32
  def self.parse_from_payload(payload)
33
- version, services, timestamp, local_addr, remote_addr, nonce, rest = payload.unpack("VQQa26a26Qa*")
33
+ version, services, timestamp, local_addr, remote_addr, nonce, rest = payload.unpack("VQ<Q<a26a26Q<a*")
34
34
  v = new
35
35
  v.version = version
36
36
  v.services = services
@@ -48,10 +48,10 @@ module Tapyrus
48
48
 
49
49
  def to_payload
50
50
  [
51
- [version, services, timestamp].pack("VQQ"),
51
+ [version, services, timestamp].pack("VQ<Q<"),
52
52
  local_addr.to_payload(true),
53
53
  remote_addr.to_payload(true),
54
- [nonce].pack("Q"),
54
+ [nonce].pack("Q<"),
55
55
  pack_var_string(user_agent),
56
56
  [start_height].pack("V"),
57
57
  pack_boolean(relay)
@@ -24,7 +24,7 @@ module Tapyrus
24
24
  mnemonic =
25
25
  words
26
26
  .map do |w|
27
- index = word_master.index(w.downcase)
27
+ index = word_master.index(normalize(w).downcase)
28
28
  raise IndexError, "word not found in words list." unless index
29
29
  index.to_s(2).rjust(11, "0")
30
30
  end
@@ -55,8 +55,8 @@ module Tapyrus
55
55
  def to_seed(mnemonic, passphrase: "")
56
56
  to_entropy(mnemonic)
57
57
  OpenSSL::PKCS5.pbkdf2_hmac(
58
- mnemonic.join(" ").downcase,
59
- "mnemonic" + passphrase,
58
+ normalize(mnemonic.join(" ")).downcase,
59
+ "mnemonic" + normalize(passphrase),
60
60
  2048,
61
61
  64,
62
62
  OpenSSL::Digest::SHA512.new
@@ -75,7 +75,12 @@ module Tapyrus
75
75
 
76
76
  # load word list contents
77
77
  def load_words
78
- File.readlines("#{WORD_DIR}/#{word_list}.txt").map(&:strip)
78
+ File.readlines("#{WORD_DIR}/#{word_list}.txt", encoding: Encoding::UTF_8).map { |w| normalize(w.strip) }
79
+ end
80
+
81
+ # BIP-39 requires that mnemonic sentences and passphrases are normalized using NFKD.
82
+ def normalize(str)
83
+ str.unicode_normalize(:nfkd)
79
84
  end
80
85
  end
81
86
  end