sibit 0.28.0 → 0.29.1

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.
@@ -6,53 +6,52 @@
6
6
  require 'digest'
7
7
  require_relative 'base58'
8
8
 
9
+ # Sibit main class.
9
10
  class Sibit
10
- module Bitcoin
11
- # Bitcoin Script parser.
12
- #
13
- # Parses standard P2PKH scripts to extract addresses.
14
- #
15
- # Author:: Yegor Bugayenko (yegor256@gmail.com)
16
- # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
17
- # License:: MIT
18
- class Script
19
- OP_DUP = 0x76
20
- OP_HASH160 = 0xa9
21
- OP_EQUALVERIFY = 0x88
22
- OP_CHECKSIG = 0xac
23
-
24
- def initialize(hex)
25
- @bytes = [hex].pack('H*').bytes
26
- end
27
-
28
- def address
29
- return p2pkh_address if p2pkh?
30
- nil
31
- end
32
-
33
- def p2pkh?
34
- @bytes.length == 25 &&
35
- @bytes[0] == OP_DUP &&
36
- @bytes[1] == OP_HASH160 &&
37
- @bytes[2] == 20 &&
38
- @bytes[23] == OP_EQUALVERIFY &&
39
- @bytes[24] == OP_CHECKSIG
40
- end
41
-
42
- def hash160
43
- return nil unless p2pkh?
44
- @bytes[3, 20].pack('C*').unpack1('H*')
45
- end
46
-
47
- private
48
-
49
- def p2pkh_address
50
- h = hash160
51
- return nil unless h
52
- versioned = "00#{h}"
53
- checksum = Base58.check(versioned)
54
- Base58.encode(versioned + checksum)
55
- end
11
+ # Bitcoin Script parser.
12
+ #
13
+ # Parses standard P2PKH scripts to extract addresses.
14
+ #
15
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
16
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
17
+ # License:: MIT
18
+ class Script
19
+ OP_DUP = 0x76
20
+ OP_HASH160 = 0xa9
21
+ OP_EQUALVERIFY = 0x88
22
+ OP_CHECKSIG = 0xac
23
+
24
+ def initialize(hex)
25
+ @bytes = [hex].pack('H*').bytes
26
+ end
27
+
28
+ def address
29
+ return p2pkh_address if p2pkh?
30
+ nil
31
+ end
32
+
33
+ def p2pkh?
34
+ @bytes.length == 25 &&
35
+ @bytes[0] == OP_DUP &&
36
+ @bytes[1] == OP_HASH160 &&
37
+ @bytes[2] == 20 &&
38
+ @bytes[23] == OP_EQUALVERIFY &&
39
+ @bytes[24] == OP_CHECKSIG
40
+ end
41
+
42
+ def hash160
43
+ return nil unless p2pkh?
44
+ @bytes[3, 20].pack('C*').unpack1('H*')
45
+ end
46
+
47
+ private
48
+
49
+ def p2pkh_address
50
+ h = hash160
51
+ return nil unless h
52
+ versioned = "00#{h}"
53
+ checksum = Base58.new(versioned).check
54
+ Base58.new(versioned + checksum).encode
56
55
  end
57
56
  end
58
57
  end
@@ -8,160 +8,61 @@ require_relative 'base58'
8
8
  require_relative 'key'
9
9
  require_relative 'script'
10
10
 
11
+ # Sibit main class.
11
12
  class Sibit
12
- module Bitcoin
13
- # Bitcoin Transaction structure.
14
- #
15
- # Author:: Yegor Bugayenko (yegor256@gmail.com)
16
- # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
17
- # License:: MIT
18
- class Tx
19
- SIGHASH_ALL = 0x01
20
- VERSION = 1
21
- SEQUENCE = 0xffffffff
22
-
23
- attr_reader :inputs, :outputs
24
-
25
- def initialize
26
- @inputs = []
27
- @outputs = []
28
- end
29
-
30
- def add_input(hash:, index:, script:, key:)
31
- @inputs << Input.new(hash, index, script, key)
32
- end
33
-
34
- def add_output(value, address)
35
- @outputs << Output.new(value, address)
36
- end
37
-
38
- def hash
39
- Digest::SHA256.hexdigest(Digest::SHA256.digest(payload)).reverse.scan(/../).join
40
- end
41
-
42
- def payload
43
- sign_inputs
44
- serialize
45
- end
46
-
47
- def hex
48
- payload.unpack1('H*')
49
- end
50
-
51
- def in
52
- @inputs
53
- end
54
-
55
- def out
56
- @outputs
57
- end
58
-
59
- private
60
-
61
- def sign_inputs
62
- @inputs.each_with_index do |input, idx|
63
- sighash = signature_hash(idx)
64
- sig = sign(input.key, sighash)
65
- pubkey = [input.key.pub].pack('H*')
66
- input.script_sig = der_sig(sig) + pubkey_script(pubkey)
67
- end
68
- end
69
-
70
- def signature_hash(idx)
71
- tx_copy = serialize_for_signing(idx)
72
- hash_type = [SIGHASH_ALL].pack('V')
73
- Digest::SHA256.digest(Digest::SHA256.digest(tx_copy + hash_type))
74
- end
75
-
76
- def sign(key, hash)
77
- der = key.sign(hash)
78
- repack(der)
79
- end
13
+ MIN_TX_FEE = 10_000
14
+
15
+ # Bitcoin Transaction structure.
16
+ #
17
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
18
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
19
+ # License:: MIT
20
+ class Tx
21
+ SIGHASH_ALL = 0x01
22
+ VERSION = 1
23
+ SEQUENCE = 0xffffffff
24
+
25
+ attr_reader :inputs, :outputs
26
+
27
+ def initialize
28
+ @inputs = []
29
+ @outputs = []
30
+ end
80
31
 
81
- def repack(der)
82
- return der if low_s?(der)
83
- seq = OpenSSL::ASN1.decode(der)
84
- r = seq.value[0].value.to_i
85
- s = seq.value[1].value.to_i
86
- order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
87
- s = order - s if s > order / 2
88
- OpenSSL::ASN1::Sequence.new(
89
- [OpenSSL::ASN1::Integer.new(r), OpenSSL::ASN1::Integer.new(s)]
90
- ).to_der
91
- end
32
+ def add_input(hash:, index:, script:, key:)
33
+ @inputs << Input.new(hash, index, script, key)
34
+ end
92
35
 
93
- def low_s?(der)
94
- seq = OpenSSL::ASN1.decode(der)
95
- s = seq.value[1].value.to_i
96
- order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
97
- s <= order / 2
98
- end
36
+ def add_output(value, address)
37
+ @outputs << Output.new(value, address)
38
+ end
99
39
 
100
- def der_sig(sig)
101
- data = sig + [SIGHASH_ALL].pack('C')
102
- [data.length].pack('C') + data
103
- end
40
+ def hash
41
+ Digest::SHA256.hexdigest(Digest::SHA256.digest(payload)).reverse.scan(/../).join
42
+ end
104
43
 
105
- def pubkey_script(pubkey)
106
- [pubkey.length].pack('C') + pubkey
107
- end
44
+ def payload
45
+ sign_inputs
46
+ serialize
47
+ end
108
48
 
109
- def serialize
110
- result = [VERSION].pack('V')
111
- result += varint(@inputs.length)
112
- @inputs.each do |input|
113
- result += [input.hash].pack('H*').reverse
114
- result += [input.index].pack('V')
115
- result += varint(input.script_sig.length)
116
- result += input.script_sig
117
- result += [SEQUENCE].pack('V')
118
- end
119
- result += varint(@outputs.length)
120
- @outputs.each do |output|
121
- result += [output.value].pack('Q<')
122
- script = output.script
123
- result += varint(script.length)
124
- result += script
125
- end
126
- result += [0].pack('V')
127
- result
128
- end
49
+ def hex
50
+ payload.unpack1('H*')
51
+ end
129
52
 
130
- def serialize_for_signing(idx)
131
- result = [VERSION].pack('V')
132
- result += varint(@inputs.length)
133
- @inputs.each_with_index do |input, i|
134
- result += [input.hash].pack('H*').reverse
135
- result += [input.index].pack('V')
136
- if i == idx
137
- script = [input.prev_script].pack('H*')
138
- result += varint(script.length)
139
- result += script
140
- else
141
- result += varint(0)
142
- end
143
- result += [SEQUENCE].pack('V')
144
- end
145
- result += varint(@outputs.length)
146
- @outputs.each do |output|
147
- result += [output.value].pack('Q<')
148
- script = output.script
149
- result += varint(script.length)
150
- result += script
151
- end
152
- result += [0].pack('V')
153
- result
154
- end
53
+ def in
54
+ @inputs
55
+ end
155
56
 
156
- def varint(num)
157
- return [num].pack('C') if num < 0xfd
158
- return [0xfd, num].pack('Cv') if num <= 0xffff
159
- return [0xfe, num].pack('CV') if num <= 0xffffffff
160
- [0xff, num].pack('CQ<')
161
- end
57
+ def out
58
+ @outputs
162
59
  end
163
60
 
164
61
  # Transaction input.
62
+ #
63
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
64
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
65
+ # License:: MIT
165
66
  class Input
166
67
  attr_reader :hash, :index, :prev_script, :key
167
68
  attr_accessor :script_sig
@@ -184,6 +85,10 @@ class Sibit
184
85
  end
185
86
 
186
87
  # Transaction output.
88
+ #
89
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
90
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
91
+ # License:: MIT
187
92
  class Output
188
93
  attr_reader :value
189
94
 
@@ -204,9 +109,113 @@ class Sibit
204
109
  private
205
110
 
206
111
  def address_to_hash160(addr)
207
- decoded = Base58.decode(addr)
112
+ decoded = Base58.new(addr).decode
208
113
  decoded[2..41]
209
114
  end
210
115
  end
116
+
117
+ private
118
+
119
+ def sign_inputs
120
+ @inputs.each_with_index do |input, idx|
121
+ sighash = signature_hash(idx)
122
+ sig = sign(input.key, sighash)
123
+ pubkey = [input.key.pub].pack('H*')
124
+ input.script_sig = der_sig(sig) + pubkey_script(pubkey)
125
+ end
126
+ end
127
+
128
+ def signature_hash(idx)
129
+ tx_copy = serialize_for_signing(idx)
130
+ hash_type = [SIGHASH_ALL].pack('V')
131
+ Digest::SHA256.digest(Digest::SHA256.digest(tx_copy + hash_type))
132
+ end
133
+
134
+ def sign(key, hash)
135
+ der = key.sign(hash)
136
+ repack(der)
137
+ end
138
+
139
+ def repack(der)
140
+ return der if low_s?(der)
141
+ seq = OpenSSL::ASN1.decode(der)
142
+ r = seq.value[0].value.to_i
143
+ s = seq.value[1].value.to_i
144
+ order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
145
+ s = order - s if s > order / 2
146
+ OpenSSL::ASN1::Sequence.new(
147
+ [OpenSSL::ASN1::Integer.new(r), OpenSSL::ASN1::Integer.new(s)]
148
+ ).to_der
149
+ end
150
+
151
+ def low_s?(der)
152
+ seq = OpenSSL::ASN1.decode(der)
153
+ s = seq.value[1].value.to_i
154
+ order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
155
+ s <= order / 2
156
+ end
157
+
158
+ def der_sig(sig)
159
+ data = sig + [SIGHASH_ALL].pack('C')
160
+ [data.length].pack('C') + data
161
+ end
162
+
163
+ def pubkey_script(pubkey)
164
+ [pubkey.length].pack('C') + pubkey
165
+ end
166
+
167
+ def serialize
168
+ result = [VERSION].pack('V')
169
+ result += varint(@inputs.length)
170
+ @inputs.each do |input|
171
+ result += [input.hash].pack('H*').reverse
172
+ result += [input.index].pack('V')
173
+ result += varint(input.script_sig.length)
174
+ result += input.script_sig
175
+ result += [SEQUENCE].pack('V')
176
+ end
177
+ result += varint(@outputs.length)
178
+ @outputs.each do |output|
179
+ result += [output.value].pack('Q<')
180
+ script = output.script
181
+ result += varint(script.length)
182
+ result += script
183
+ end
184
+ result += [0].pack('V')
185
+ result
186
+ end
187
+
188
+ def serialize_for_signing(idx)
189
+ result = [VERSION].pack('V')
190
+ result += varint(@inputs.length)
191
+ @inputs.each_with_index do |input, i|
192
+ result += [input.hash].pack('H*').reverse
193
+ result += [input.index].pack('V')
194
+ if i == idx
195
+ script = [input.prev_script].pack('H*')
196
+ result += varint(script.length)
197
+ result += script
198
+ else
199
+ result += varint(0)
200
+ end
201
+ result += [SEQUENCE].pack('V')
202
+ end
203
+ result += varint(@outputs.length)
204
+ @outputs.each do |output|
205
+ result += [output.value].pack('Q<')
206
+ script = output.script
207
+ result += varint(script.length)
208
+ result += script
209
+ end
210
+ result += [0].pack('V')
211
+ result
212
+ end
213
+
214
+ def varint(num)
215
+ return [num].pack('C') if num < 0xfd
216
+ return [0xfd, num].pack('Cv') if num <= 0xffff
217
+ return [0xfe, num].pack('CV') if num <= 0xffffffff
218
+ [0xff, num].pack('CQ<')
219
+ end
211
220
  end
212
221
  end
@@ -3,57 +3,60 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- require_relative 'tx'
7
6
  require_relative 'key'
7
+ require_relative 'tx'
8
8
 
9
+ # Sibit main class.
9
10
  class Sibit
10
- module Bitcoin
11
- # Bitcoin Transaction Builder.
12
- #
13
- # Provides a similar interface to Bitcoin::Builder::TxBuilder for
14
- # building and signing Bitcoin transactions.
15
- #
16
- # Author:: Yegor Bugayenko (yegor256@gmail.com)
17
- # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
18
- # License:: MIT
19
- class TxBuilder
20
- def initialize
21
- @inputs = []
22
- @outputs = []
23
- end
11
+ # Bitcoin Transaction Builder.
12
+ #
13
+ # Provides a similar interface to Bitcoin::Builder::TxBuilder for
14
+ # building and signing Bitcoin transactions.
15
+ #
16
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
17
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
18
+ # License:: MIT
19
+ class TxBuilder
20
+ def initialize
21
+ @inputs = []
22
+ @outputs = []
23
+ end
24
24
 
25
- def input
26
- inp = InputBuilder.new
27
- yield inp
28
- @inputs << inp
29
- end
25
+ def input
26
+ inp = Input.new
27
+ yield inp
28
+ @inputs << inp
29
+ end
30
30
 
31
- def output(value, address)
32
- @outputs << { value: value, address: address }
33
- end
31
+ def output(value, address)
32
+ @outputs << { value: value, address: address }
33
+ end
34
34
 
35
- def tx(input_value:, leave_fee:, extra_fee:, change_address:)
36
- txn = Tx.new
37
- @inputs.each do |inp|
38
- txn.add_input(
39
- hash: inp.prev_out_hash,
40
- index: inp.prev_out_idx,
41
- script: inp.script,
42
- key: inp.key
43
- )
44
- end
45
- total_out = @outputs.sum { |o| o[:value] }
46
- @outputs.each { |o| txn.add_output(o[:value], o[:address]) }
47
- if leave_fee
48
- change = input_value - total_out - extra_fee
49
- txn.add_output(change, change_address) if change.positive?
50
- end
51
- BuiltTx.new(txn, @inputs, @outputs)
52
- end
35
+ def tx(input_value:, leave_fee:, extra_fee:, change_address:)
36
+ txn = Tx.new
37
+ @inputs.each do |inp|
38
+ txn.add_input(
39
+ hash: inp.prev_out_hash,
40
+ index: inp.prev_out_idx,
41
+ script: inp.script,
42
+ key: inp.key
43
+ )
44
+ end
45
+ total_out = @outputs.sum { |o| o[:value] }
46
+ @outputs.each { |o| txn.add_output(o[:value], o[:address]) }
47
+ if leave_fee
48
+ change = input_value - total_out - extra_fee
49
+ txn.add_output(change, change_address) if change.positive?
50
+ end
51
+ Built.new(txn, @inputs, @outputs)
53
52
  end
54
53
 
55
54
  # Input builder for collecting input parameters.
56
- class InputBuilder
55
+ #
56
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
57
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
58
+ # License:: MIT
59
+ class Input
57
60
  attr_reader :prev_out_hash, :prev_out_idx, :script, :key
58
61
 
59
62
  def prev_out(hash)
@@ -74,11 +77,15 @@ class Sibit
74
77
  end
75
78
 
76
79
  # Wrapper for built transaction with convenience methods.
77
- class BuiltTx
80
+ #
81
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
82
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
83
+ # License:: MIT
84
+ class Built
78
85
  def initialize(txn, inputs, outputs)
79
86
  @tx = txn
80
- @inputs_data = inputs
81
- @outputs_data = outputs
87
+ @inputs = inputs
88
+ @outputs = outputs
82
89
  end
83
90
 
84
91
  def hash
@@ -102,18 +109,22 @@ class Sibit
102
109
  end
103
110
 
104
111
  def to_payload
105
- PayloadWrapper.new(@tx.payload)
112
+ Payload.new(@tx.payload)
106
113
  end
107
- end
108
114
 
109
- # Wrapper for payload with hex conversion.
110
- class PayloadWrapper
111
- def initialize(bytes)
112
- @bytes = bytes
113
- end
115
+ # Wrapper for payload with hex conversion.
116
+ #
117
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
118
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
119
+ # License:: MIT
120
+ class Payload
121
+ def initialize(bytes)
122
+ @bytes = bytes
123
+ end
114
124
 
115
- def bth
116
- @bytes.unpack1('H*')
125
+ def bth
126
+ @bytes.unpack1('H*')
127
+ end
117
128
  end
118
129
  end
119
130
  end