bsv-sdk 0.15.0 → 0.17.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/lib/bsv/auth/auth_middleware.rb +6 -6
  4. data/lib/bsv/auth/certificate.rb +22 -18
  5. data/lib/bsv/auth/master_certificate.rb +5 -5
  6. data/lib/bsv/auth/nonce.rb +13 -13
  7. data/lib/bsv/auth/peer.rb +53 -53
  8. data/lib/bsv/auth/verifiable_certificate.rb +1 -1
  9. data/lib/bsv/identity/client.rb +27 -32
  10. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +18 -12
  11. data/lib/bsv/mcp/tools/check_balance.rb +16 -4
  12. data/lib/bsv/mcp/tools/fetch_tx.rb +11 -4
  13. data/lib/bsv/mcp/tools/fetch_utxos.rb +16 -4
  14. data/lib/bsv/mcp/tools/helpers.rb +2 -2
  15. data/lib/bsv/network/arc.rb +13 -153
  16. data/lib/bsv/network/broadcast_error.rb +1 -0
  17. data/lib/bsv/network/broadcast_response.rb +1 -0
  18. data/lib/bsv/network/protocols/arc.rb +4 -3
  19. data/lib/bsv/network/protocols/taal_binary.rb +1 -0
  20. data/lib/bsv/network/protocols/woc_rest.rb +2 -1
  21. data/lib/bsv/network/whats_on_chain.rb +13 -107
  22. data/lib/bsv/overlay/admin_token_template.rb +4 -4
  23. data/lib/bsv/overlay/lookup_resolver.rb +1 -0
  24. data/lib/bsv/overlay/topic_broadcaster.rb +1 -1
  25. data/lib/bsv/overlay/types.rb +1 -0
  26. data/lib/bsv/primitives/hex.rb +64 -0
  27. data/lib/bsv/registry/client.rb +26 -28
  28. data/lib/bsv/registry/types.rb +1 -0
  29. data/lib/bsv/script/interpreter/interpreter.rb +7 -0
  30. data/lib/bsv/script/interpreter/operations/crypto.rb +7 -1
  31. data/lib/bsv/script/push_drop_template.rb +4 -4
  32. data/lib/bsv/transaction/beef.rb +122 -83
  33. data/lib/bsv/transaction/merkle_path.rb +54 -38
  34. data/lib/bsv/transaction/transaction.rb +81 -30
  35. data/lib/bsv/transaction/transaction_input.rb +23 -18
  36. data/lib/bsv/version.rb +1 -1
  37. data/lib/bsv/wallet/errors.rb +47 -0
  38. data/lib/bsv/wallet/interface/brc100.rb +270 -0
  39. data/lib/bsv/wallet/interface.rb +9 -0
  40. data/lib/bsv/wallet/proto_wallet/key_deriver.rb +152 -0
  41. data/lib/bsv/wallet/proto_wallet/validators.rb +74 -0
  42. data/lib/bsv/wallet/proto_wallet.rb +327 -0
  43. data/lib/bsv/wallet.rb +16 -0
  44. data/lib/bsv-sdk.rb +18 -1
  45. metadata +22 -1
@@ -0,0 +1,327 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require_relative 'proto_wallet/validators'
5
+ require_relative 'proto_wallet/key_deriver'
6
+
7
+ module BSV
8
+ module Wallet
9
+ # Minimal cryptographic wallet implementing the BRC-100 interface.
10
+ #
11
+ # ProtoWallet provides signing, encryption, HMAC, and key derivation
12
+ # without transactions, storage, or blockchain interaction. It is the
13
+ # direct implementation of the BRC-100 crypto methods, not a delegating
14
+ # client. This makes it suitable for use in the SDK's Auth module without
15
+ # depending on the bsv-wallet gem.
16
+ #
17
+ # Includes +BSV::Wallet::Interface::BRC100+ — methods it supports are
18
+ # overridden; unsupported methods (transactions, blockchain, authentication)
19
+ # fall through to +NotImplementedError+.
20
+ #
21
+ class ProtoWallet
22
+ include Interface::BRC100
23
+
24
+ # @param root_key [BSV::Primitives::PrivateKey, String] a private key or 'anyone'
25
+ def initialize(root_key)
26
+ @key_deriver = KeyDeriver.new(root_key)
27
+ end
28
+
29
+ # Returns a derived or identity public key.
30
+ #
31
+ # @param identity_key [Boolean] return the root identity key
32
+ # @param protocol_id [Array] [security_level, protocol_name]
33
+ # @param key_id [String] key identifier
34
+ # @param counterparty [String] pubkey hex, 'self', or 'anyone'
35
+ # @param for_self [Boolean] derive from own identity
36
+ # @return [Hash] { public_key: String } hex-encoded compressed public key
37
+ def get_public_key(identity_key: false, protocol_id: nil, key_id: nil,
38
+ counterparty: nil, for_self: false,
39
+ privileged: false, privileged_reason: nil,
40
+ seek_permission: true, originator: nil)
41
+ if identity_key
42
+ { public_key: @key_deriver.identity_key }
43
+ else
44
+ counterparty ||= 'self'
45
+ pub = @key_deriver.derive_public_key(
46
+ protocol_id, key_id, counterparty, for_self: for_self
47
+ )
48
+ { public_key: pub.to_hex }
49
+ end
50
+ end
51
+
52
+ # Encrypts plaintext using AES-256-GCM with a derived symmetric key.
53
+ #
54
+ # @param plaintext [Array<Integer>] byte array to encrypt
55
+ # @param protocol_id [Array] [security_level, protocol_name]
56
+ # @param key_id [String] key identifier
57
+ # @param counterparty [String] pubkey hex, 'self', or 'anyone'
58
+ # @return [Hash] { ciphertext: Array<Integer> }
59
+ def encrypt(plaintext:, protocol_id:, key_id:,
60
+ counterparty: nil, privileged: false, privileged_reason: nil,
61
+ seek_permission: true, originator: nil)
62
+ sym_key = derive_sym_key(protocol_id, key_id, counterparty)
63
+ ciphertext = sym_key.encrypt(bytes_to_string(plaintext))
64
+ { ciphertext: string_to_bytes(ciphertext) }
65
+ end
66
+
67
+ # Decrypts ciphertext using AES-256-GCM with a derived symmetric key.
68
+ #
69
+ # @param ciphertext [Array<Integer>] byte array to decrypt
70
+ # @param protocol_id [Array] [security_level, protocol_name]
71
+ # @param key_id [String] key identifier
72
+ # @param counterparty [String] pubkey hex, 'self', or 'anyone'
73
+ # @return [Hash] { plaintext: Array<Integer> }
74
+ def decrypt(ciphertext:, protocol_id:, key_id:,
75
+ counterparty: nil, privileged: false, privileged_reason: nil,
76
+ seek_permission: true, originator: nil)
77
+ sym_key = derive_sym_key(protocol_id, key_id, counterparty)
78
+ plaintext = sym_key.decrypt(bytes_to_string(ciphertext))
79
+ { plaintext: string_to_bytes(plaintext) }
80
+ end
81
+
82
+ # Creates an HMAC-SHA256 using a derived symmetric key.
83
+ #
84
+ # @param data [Array<Integer>] byte array to authenticate
85
+ # @param protocol_id [Array] [security_level, protocol_name]
86
+ # @param key_id [String] key identifier
87
+ # @param counterparty [String] pubkey hex, 'self', or 'anyone'
88
+ # @return [Hash] { hmac: Array<Integer> }
89
+ def create_hmac(data:, protocol_id:, key_id:,
90
+ counterparty: nil, privileged: false, privileged_reason: nil,
91
+ seek_permission: true, originator: nil)
92
+ sym_key = derive_sym_key(protocol_id, key_id, counterparty)
93
+ hmac = BSV::Primitives::Digest.hmac_sha256(sym_key.to_bytes, bytes_to_string(data))
94
+ { hmac: string_to_bytes(hmac) }
95
+ end
96
+
97
+ # Verifies an HMAC-SHA256 using a derived symmetric key.
98
+ #
99
+ # @param data [Array<Integer>] byte array that was authenticated
100
+ # @param hmac [Array<Integer>] HMAC to verify
101
+ # @param protocol_id [Array] [security_level, protocol_name]
102
+ # @param key_id [String] key identifier
103
+ # @param counterparty [String] pubkey hex, 'self', or 'anyone'
104
+ # @return [Hash] { valid: true }
105
+ # @raise [InvalidHmacError] if the HMAC does not match
106
+ def verify_hmac(data:, hmac:, protocol_id:, key_id:,
107
+ counterparty: nil, privileged: false, privileged_reason: nil,
108
+ seek_permission: true, originator: nil)
109
+ sym_key = derive_sym_key(protocol_id, key_id, counterparty)
110
+ expected = BSV::Primitives::Digest.hmac_sha256(sym_key.to_bytes, bytes_to_string(data))
111
+ provided = bytes_to_string(hmac)
112
+
113
+ raise InvalidHmacError unless secure_compare(expected, provided)
114
+
115
+ { valid: true }
116
+ end
117
+
118
+ # Creates an ECDSA signature using a derived private key.
119
+ #
120
+ # @param protocol_id [Array] [security_level, protocol_name]
121
+ # @param key_id [String] key identifier
122
+ # @param data [Array<Integer>] data to hash and sign
123
+ # @param hash_to_directly_sign [Array<Integer>] pre-computed 32-byte hash
124
+ # @param counterparty [String] pubkey hex, 'self', or 'anyone'
125
+ # @return [Hash] { signature: Array<Integer> } DER-encoded signature as byte array
126
+ def create_signature(protocol_id:, key_id:, data: nil, hash_to_directly_sign: nil,
127
+ counterparty: nil, privileged: false, privileged_reason: nil,
128
+ seek_permission: true, originator: nil)
129
+ counterparty ||= 'anyone'
130
+ BSV.logger&.debug { "[ProtoWallet] create_signature: protocol=#{protocol_id} key_id=#{key_id.inspect} counterparty=#{counterparty}" }
131
+ priv_key = @key_deriver.derive_private_key(protocol_id, key_id, counterparty)
132
+
133
+ hash = if hash_to_directly_sign
134
+ bytes_to_string(hash_to_directly_sign)
135
+ else
136
+ BSV::Primitives::Digest.sha256(bytes_to_string(data))
137
+ end
138
+
139
+ sig = priv_key.sign(hash)
140
+ { signature: string_to_bytes(sig.to_der) }
141
+ end
142
+
143
+ # Verifies an ECDSA signature using a derived public key.
144
+ #
145
+ # @param signature [Array<Integer>] DER-encoded signature as byte array
146
+ # @param protocol_id [Array] [security_level, protocol_name]
147
+ # @param key_id [String] key identifier
148
+ # @param data [Array<Integer>] original data that was signed
149
+ # @param hash_to_directly_verify [Array<Integer>] pre-computed 32-byte hash
150
+ # @param counterparty [String] pubkey hex, 'self', or 'anyone'
151
+ # @param for_self [Boolean] verify own derived key (default false)
152
+ # @return [Hash] { valid: true }
153
+ # @raise [InvalidSignatureError] if the signature does not verify
154
+ def verify_signature(signature:, protocol_id:, key_id:, data: nil,
155
+ hash_to_directly_verify: nil,
156
+ counterparty: nil, for_self: false,
157
+ privileged: false, privileged_reason: nil,
158
+ seek_permission: true, originator: nil)
159
+ counterparty ||= 'self'
160
+ BSV.logger&.debug do
161
+ "[ProtoWallet] verify_signature: protocol=#{protocol_id} key_id=#{key_id.inspect} " \
162
+ "counterparty=#{counterparty} for_self=#{for_self}"
163
+ end
164
+
165
+ pub_key = @key_deriver.derive_public_key(
166
+ protocol_id, key_id, counterparty, for_self: for_self
167
+ )
168
+
169
+ hash = if hash_to_directly_verify
170
+ bytes_to_string(hash_to_directly_verify)
171
+ else
172
+ BSV::Primitives::Digest.sha256(bytes_to_string(data))
173
+ end
174
+
175
+ sig = BSV::Primitives::Signature.from_der(bytes_to_string(signature))
176
+ valid = pub_key.verify(hash, sig)
177
+ BSV.logger&.debug { "[ProtoWallet] verify_signature result=#{valid}" }
178
+
179
+ raise InvalidSignatureError unless valid
180
+
181
+ { valid: true }
182
+ end
183
+
184
+ # Reveals counterparty key linkage to a verifier (BRC-69 Method 1).
185
+ #
186
+ # @param counterparty [String] counterparty public key hex (not 'self' or 'anyone')
187
+ # @param verifier [String] verifier public key hex
188
+ # @return [Hash] { prover:, verifier:, counterparty:, revelation_time:,
189
+ # encrypted_linkage:, encrypted_linkage_proof: }
190
+ def reveal_counterparty_key_linkage(counterparty:, verifier:,
191
+ privileged: false, privileged_reason: nil,
192
+ originator: nil)
193
+ raise InvalidParameterError.new('counterparty', 'a specific public key hex, not "anyone"') if counterparty == 'anyone'
194
+
195
+ Validators.validate_pub_key_hex!(verifier, 'verifier')
196
+
197
+ linkage = @key_deriver.reveal_counterparty_secret(counterparty)
198
+ revelation_time = Time.now.utc.iso8601
199
+
200
+ encrypted_linkage_result = encrypt(
201
+ plaintext: string_to_bytes(linkage),
202
+ protocol_id: [2, 'counterparty linkage revelation'],
203
+ key_id: revelation_time,
204
+ counterparty: verifier
205
+ )
206
+
207
+ counterparty_pub = BSV::Primitives::PublicKey.from_hex(counterparty)
208
+ linkage_point = BSV::Primitives::PublicKey.from_bytes(linkage)
209
+ schnorr_proof = BSV::Primitives::Schnorr.generate_proof(
210
+ @key_deriver.root_key,
211
+ @key_deriver.root_key.public_key,
212
+ counterparty_pub,
213
+ linkage_point
214
+ )
215
+
216
+ z_bytes = schnorr_proof.z.to_s(2)
217
+ z_bytes = ("\x00".b * (32 - z_bytes.length)) + z_bytes if z_bytes.length < 32
218
+ proof_bin = schnorr_proof.r.compressed + schnorr_proof.s_prime.compressed + z_bytes
219
+
220
+ encrypted_proof_result = encrypt(
221
+ plaintext: string_to_bytes(proof_bin),
222
+ protocol_id: [2, 'counterparty linkage revelation'],
223
+ key_id: revelation_time,
224
+ counterparty: verifier
225
+ )
226
+
227
+ {
228
+ prover: @key_deriver.identity_key,
229
+ verifier: verifier,
230
+ counterparty: counterparty,
231
+ revelation_time: revelation_time,
232
+ encrypted_linkage: encrypted_linkage_result[:ciphertext],
233
+ encrypted_linkage_proof: encrypted_proof_result[:ciphertext]
234
+ }
235
+ end
236
+
237
+ # Reveals specific key linkage for a particular interaction (BRC-69 Method 2).
238
+ #
239
+ # @param counterparty [String] counterparty public key hex
240
+ # @param verifier [String] verifier public key hex
241
+ # @param protocol_id [Array] [security_level, protocol_name]
242
+ # @param key_id [String] key identifier
243
+ # @return [Hash] { prover:, verifier:, counterparty:, protocol_id:, key_id:,
244
+ # encrypted_linkage:, encrypted_linkage_proof:, proof_type: }
245
+ def reveal_specific_key_linkage(counterparty:, verifier:, protocol_id:, key_id:,
246
+ privileged: false, privileged_reason: nil,
247
+ originator: nil)
248
+ raise InvalidParameterError.new('counterparty', 'a specific public key hex, not "anyone"') if counterparty == 'anyone'
249
+
250
+ Validators.validate_pub_key_hex!(verifier, 'verifier')
251
+
252
+ linkage = @key_deriver.reveal_specific_secret(counterparty, protocol_id, key_id)
253
+ derived_protocol = "specific linkage revelation #{protocol_id[0]} #{protocol_id[1]}"
254
+
255
+ encrypted_linkage_result = encrypt(
256
+ plaintext: string_to_bytes(linkage),
257
+ protocol_id: [2, derived_protocol],
258
+ key_id: key_id,
259
+ counterparty: verifier
260
+ )
261
+
262
+ encrypted_proof_result = encrypt(
263
+ plaintext: [0],
264
+ protocol_id: [2, derived_protocol],
265
+ key_id: key_id,
266
+ counterparty: verifier
267
+ )
268
+
269
+ {
270
+ prover: @key_deriver.identity_key,
271
+ verifier: verifier,
272
+ counterparty: counterparty,
273
+ protocol_id: protocol_id,
274
+ key_id: key_id,
275
+ encrypted_linkage: encrypted_linkage_result[:ciphertext],
276
+ encrypted_linkage_proof: encrypted_proof_result[:ciphertext],
277
+ proof_type: 0
278
+ }
279
+ end
280
+
281
+ # Returns an empty certificate list.
282
+ #
283
+ # ProtoWallet has no storage, so there are never any certificates.
284
+ #
285
+ # @return [Hash] { certificates: [] }
286
+ def list_certificates(certifiers: nil, types: nil, limit: 10, offset: 0,
287
+ privileged: false, privileged_reason: nil, originator: nil)
288
+ { certificates: [] }
289
+ end
290
+
291
+ # Not supported — ProtoWallet has no certificate storage.
292
+ #
293
+ # @raise [UnsupportedActionError] always
294
+ def prove_certificate(certificate: nil, fields_to_reveal: nil, verifier: nil,
295
+ privileged: false, privileged_reason: nil, originator: nil)
296
+ raise UnsupportedActionError, 'prove_certificate'
297
+ end
298
+
299
+ private
300
+
301
+ def derive_sym_key(protocol_id, key_id, counterparty)
302
+ counterparty ||= 'self'
303
+ @key_deriver.derive_symmetric_key(protocol_id, key_id, counterparty)
304
+ end
305
+
306
+ def bytes_to_string(bytes)
307
+ bytes.pack('C*')
308
+ end
309
+
310
+ def string_to_bytes(str)
311
+ str.unpack('C*')
312
+ end
313
+
314
+ def secure_compare(a, b)
315
+ return false unless a.bytesize == b.bytesize
316
+
317
+ if OpenSSL.respond_to?(:fixed_length_secure_compare)
318
+ OpenSSL.fixed_length_secure_compare(a, b)
319
+ else
320
+ result = 0
321
+ a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
322
+ result.zero?
323
+ end
324
+ end
325
+ end
326
+ end
327
+ end
data/lib/bsv/wallet.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # Shared contract: BRC-100 error classes and interface definition.
6
+ # These are the canonical definitions consumed by both bsv-sdk (ProtoWallet)
7
+ # and bsv-wallet (Engine).
8
+ require_relative 'wallet/errors'
9
+ require_relative 'wallet/interface'
10
+
11
+ # ProtoWallet — minimal crypto-only BRC-100 implementation.
12
+ # Its internals (KeyDeriver, Validators) are scoped under ProtoWallet::
13
+ # to avoid collision with bsv-wallet's own KeyDeriver.
14
+ require_relative 'wallet/proto_wallet'
15
+ end
16
+ end
data/lib/bsv-sdk.rb CHANGED
@@ -3,6 +3,20 @@
3
3
  require_relative 'bsv/version'
4
4
 
5
5
  module BSV
6
+ class << self
7
+ # Optional logger for debug-level instrumentation of txid conversions,
8
+ # BEEF wiring, and merkle path operations.
9
+ #
10
+ # No logger is configured by default — zero overhead when unused.
11
+ # Consumers opt in via:
12
+ #
13
+ # require 'logger'
14
+ # BSV.logger = Logger.new($stdout).tap { |l| l.level = Logger::DEBUG }
15
+ #
16
+ # @return [Logger, nil]
17
+ attr_accessor :logger
18
+ end
19
+
6
20
  autoload :Primitives, 'bsv/primitives'
7
21
  autoload :Script, 'bsv/script'
8
22
  autoload :Transaction, 'bsv/transaction'
@@ -13,5 +27,8 @@ module BSV
13
27
  autoload :Registry, 'bsv/registry'
14
28
  autoload :MCP, 'bsv/mcp'
15
29
 
16
- autoload :WireFormat, 'bsv/wire_format'
30
+ # Wallet is loaded eagerly to avoid load-path shadowing when bsv-wallet is
31
+ # also in the bundle (bsv-wallet/lib/bsv/wallet.rb would otherwise win).
32
+ require_relative 'bsv/wallet'
33
+ autoload :WireFormat, 'bsv/wire_format'
17
34
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bsv-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Bettison
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: mcp
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -177,6 +191,13 @@ files:
177
191
  - lib/bsv/transaction/var_int.rb
178
192
  - lib/bsv/transaction/verification_error.rb
179
193
  - lib/bsv/version.rb
194
+ - lib/bsv/wallet.rb
195
+ - lib/bsv/wallet/errors.rb
196
+ - lib/bsv/wallet/interface.rb
197
+ - lib/bsv/wallet/interface/brc100.rb
198
+ - lib/bsv/wallet/proto_wallet.rb
199
+ - lib/bsv/wallet/proto_wallet/key_deriver.rb
200
+ - lib/bsv/wallet/proto_wallet/validators.rb
180
201
  - lib/bsv/wire_format.rb
181
202
  homepage: https://github.com/sgbett/bsv-ruby-sdk
182
203
  licenses: