bsv-sdk 0.14.0 → 0.16.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/README.md +14 -2
  4. data/lib/bsv/auth/auth_middleware.rb +6 -6
  5. data/lib/bsv/auth/certificate.rb +16 -16
  6. data/lib/bsv/auth/master_certificate.rb +5 -5
  7. data/lib/bsv/auth/nonce.rb +13 -13
  8. data/lib/bsv/auth/peer.rb +53 -53
  9. data/lib/bsv/auth/verifiable_certificate.rb +1 -1
  10. data/lib/bsv/identity/client.rb +26 -32
  11. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +17 -11
  12. data/lib/bsv/mcp/tools/check_balance.rb +16 -4
  13. data/lib/bsv/mcp/tools/fetch_tx.rb +11 -4
  14. data/lib/bsv/mcp/tools/fetch_utxos.rb +16 -4
  15. data/lib/bsv/network/arc.rb +13 -153
  16. data/lib/bsv/network/whats_on_chain.rb +13 -107
  17. data/lib/bsv/overlay/admin_token_template.rb +4 -4
  18. data/lib/bsv/primitives/base58.rb +2 -1
  19. data/lib/bsv/primitives/curve.rb +37 -12
  20. data/lib/bsv/primitives/ecdsa.rb +4 -4
  21. data/lib/bsv/primitives/openssl_ec_shim.rb +32 -5
  22. data/lib/bsv/primitives/private_key.rb +2 -2
  23. data/lib/bsv/primitives/public_key.rb +1 -1
  24. data/lib/bsv/primitives/schnorr.rb +4 -4
  25. data/lib/bsv/primitives/secp256k1.rb +4 -595
  26. data/lib/bsv/primitives/signature.rb +2 -0
  27. data/lib/bsv/primitives/signed_message.rb +6 -5
  28. data/lib/bsv/registry/client.rb +23 -27
  29. data/lib/bsv/script/push_drop_template.rb +4 -4
  30. data/lib/bsv/secp256k1_native.bundle +0 -0
  31. data/lib/bsv/version.rb +1 -1
  32. data/lib/bsv/wallet/errors.rb +47 -0
  33. data/lib/bsv/wallet/interface/brc100.rb +267 -0
  34. data/lib/bsv/wallet/interface.rb +9 -0
  35. data/lib/bsv/wallet/proto_wallet/key_deriver.rb +150 -0
  36. data/lib/bsv/wallet/proto_wallet/validators.rb +74 -0
  37. data/lib/bsv/wallet/proto_wallet.rb +321 -0
  38. data/lib/bsv/wallet.rb +16 -0
  39. data/lib/bsv-sdk.rb +4 -1
  40. metadata +37 -1
@@ -0,0 +1,321 @@
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
+ priv_key = @key_deriver.derive_private_key(protocol_id, key_id, counterparty)
131
+
132
+ hash = if hash_to_directly_sign
133
+ bytes_to_string(hash_to_directly_sign)
134
+ else
135
+ BSV::Primitives::Digest.sha256(bytes_to_string(data))
136
+ end
137
+
138
+ sig = priv_key.sign(hash)
139
+ { signature: string_to_bytes(sig.to_der) }
140
+ end
141
+
142
+ # Verifies an ECDSA signature using a derived public key.
143
+ #
144
+ # @param signature [Array<Integer>] DER-encoded signature as byte array
145
+ # @param protocol_id [Array] [security_level, protocol_name]
146
+ # @param key_id [String] key identifier
147
+ # @param data [Array<Integer>] original data that was signed
148
+ # @param hash_to_directly_verify [Array<Integer>] pre-computed 32-byte hash
149
+ # @param counterparty [String] pubkey hex, 'self', or 'anyone'
150
+ # @param for_self [Boolean] verify own derived key (default false)
151
+ # @return [Hash] { valid: true }
152
+ # @raise [InvalidSignatureError] if the signature does not verify
153
+ def verify_signature(signature:, protocol_id:, key_id:, data: nil,
154
+ hash_to_directly_verify: nil,
155
+ counterparty: nil, for_self: false,
156
+ privileged: false, privileged_reason: nil,
157
+ seek_permission: true, originator: nil)
158
+ counterparty ||= 'self'
159
+
160
+ pub_key = @key_deriver.derive_public_key(
161
+ protocol_id, key_id, counterparty, for_self: for_self
162
+ )
163
+
164
+ hash = if hash_to_directly_verify
165
+ bytes_to_string(hash_to_directly_verify)
166
+ else
167
+ BSV::Primitives::Digest.sha256(bytes_to_string(data))
168
+ end
169
+
170
+ sig = BSV::Primitives::Signature.from_der(bytes_to_string(signature))
171
+ valid = pub_key.verify(hash, sig)
172
+
173
+ raise InvalidSignatureError unless valid
174
+
175
+ { valid: true }
176
+ end
177
+
178
+ # Reveals counterparty key linkage to a verifier (BRC-69 Method 1).
179
+ #
180
+ # @param counterparty [String] counterparty public key hex (not 'self' or 'anyone')
181
+ # @param verifier [String] verifier public key hex
182
+ # @return [Hash] { prover:, verifier:, counterparty:, revelation_time:,
183
+ # encrypted_linkage:, encrypted_linkage_proof: }
184
+ def reveal_counterparty_key_linkage(counterparty:, verifier:,
185
+ privileged: false, privileged_reason: nil,
186
+ originator: nil)
187
+ raise InvalidParameterError.new('counterparty', 'a specific public key hex, not "anyone"') if counterparty == 'anyone'
188
+
189
+ Validators.validate_pub_key_hex!(verifier, 'verifier')
190
+
191
+ linkage = @key_deriver.reveal_counterparty_secret(counterparty)
192
+ revelation_time = Time.now.utc.iso8601
193
+
194
+ encrypted_linkage_result = encrypt(
195
+ plaintext: string_to_bytes(linkage),
196
+ protocol_id: [2, 'counterparty linkage revelation'],
197
+ key_id: revelation_time,
198
+ counterparty: verifier
199
+ )
200
+
201
+ counterparty_pub = BSV::Primitives::PublicKey.from_hex(counterparty)
202
+ linkage_point = BSV::Primitives::PublicKey.from_bytes(linkage)
203
+ schnorr_proof = BSV::Primitives::Schnorr.generate_proof(
204
+ @key_deriver.root_key,
205
+ @key_deriver.root_key.public_key,
206
+ counterparty_pub,
207
+ linkage_point
208
+ )
209
+
210
+ z_bytes = schnorr_proof.z.to_s(2)
211
+ z_bytes = ("\x00".b * (32 - z_bytes.length)) + z_bytes if z_bytes.length < 32
212
+ proof_bin = schnorr_proof.r.compressed + schnorr_proof.s_prime.compressed + z_bytes
213
+
214
+ encrypted_proof_result = encrypt(
215
+ plaintext: string_to_bytes(proof_bin),
216
+ protocol_id: [2, 'counterparty linkage revelation'],
217
+ key_id: revelation_time,
218
+ counterparty: verifier
219
+ )
220
+
221
+ {
222
+ prover: @key_deriver.identity_key,
223
+ verifier: verifier,
224
+ counterparty: counterparty,
225
+ revelation_time: revelation_time,
226
+ encrypted_linkage: encrypted_linkage_result[:ciphertext],
227
+ encrypted_linkage_proof: encrypted_proof_result[:ciphertext]
228
+ }
229
+ end
230
+
231
+ # Reveals specific key linkage for a particular interaction (BRC-69 Method 2).
232
+ #
233
+ # @param counterparty [String] counterparty public key hex
234
+ # @param verifier [String] verifier public key hex
235
+ # @param protocol_id [Array] [security_level, protocol_name]
236
+ # @param key_id [String] key identifier
237
+ # @return [Hash] { prover:, verifier:, counterparty:, protocol_id:, key_id:,
238
+ # encrypted_linkage:, encrypted_linkage_proof:, proof_type: }
239
+ def reveal_specific_key_linkage(counterparty:, verifier:, protocol_id:, key_id:,
240
+ privileged: false, privileged_reason: nil,
241
+ originator: nil)
242
+ raise InvalidParameterError.new('counterparty', 'a specific public key hex, not "anyone"') if counterparty == 'anyone'
243
+
244
+ Validators.validate_pub_key_hex!(verifier, 'verifier')
245
+
246
+ linkage = @key_deriver.reveal_specific_secret(counterparty, protocol_id, key_id)
247
+ derived_protocol = "specific linkage revelation #{protocol_id[0]} #{protocol_id[1]}"
248
+
249
+ encrypted_linkage_result = encrypt(
250
+ plaintext: string_to_bytes(linkage),
251
+ protocol_id: [2, derived_protocol],
252
+ key_id: key_id,
253
+ counterparty: verifier
254
+ )
255
+
256
+ encrypted_proof_result = encrypt(
257
+ plaintext: [0],
258
+ protocol_id: [2, derived_protocol],
259
+ key_id: key_id,
260
+ counterparty: verifier
261
+ )
262
+
263
+ {
264
+ prover: @key_deriver.identity_key,
265
+ verifier: verifier,
266
+ counterparty: counterparty,
267
+ protocol_id: protocol_id,
268
+ key_id: key_id,
269
+ encrypted_linkage: encrypted_linkage_result[:ciphertext],
270
+ encrypted_linkage_proof: encrypted_proof_result[:ciphertext],
271
+ proof_type: 0
272
+ }
273
+ end
274
+
275
+ # Returns an empty certificate list.
276
+ #
277
+ # ProtoWallet has no storage, so there are never any certificates.
278
+ #
279
+ # @return [Hash] { certificates: [] }
280
+ def list_certificates(certifiers: nil, types: nil, limit: 10, offset: 0,
281
+ privileged: false, privileged_reason: nil, originator: nil)
282
+ { certificates: [] }
283
+ end
284
+
285
+ # Not supported — ProtoWallet has no certificate storage.
286
+ #
287
+ # @raise [UnsupportedActionError] always
288
+ def prove_certificate(certificate: nil, fields_to_reveal: nil, verifier: nil,
289
+ privileged: false, privileged_reason: nil, originator: nil)
290
+ raise UnsupportedActionError, 'prove_certificate'
291
+ end
292
+
293
+ private
294
+
295
+ def derive_sym_key(protocol_id, key_id, counterparty)
296
+ counterparty ||= 'self'
297
+ @key_deriver.derive_symmetric_key(protocol_id, key_id, counterparty)
298
+ end
299
+
300
+ def bytes_to_string(bytes)
301
+ bytes.pack('C*')
302
+ end
303
+
304
+ def string_to_bytes(str)
305
+ str.unpack('C*')
306
+ end
307
+
308
+ def secure_compare(a, b)
309
+ return false unless a.bytesize == b.bytesize
310
+
311
+ if OpenSSL.respond_to?(:fixed_length_secure_compare)
312
+ OpenSSL.fixed_length_secure_compare(a, b)
313
+ else
314
+ result = 0
315
+ a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
316
+ result.zero?
317
+ end
318
+ end
319
+ end
320
+ end
321
+ 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
@@ -13,5 +13,8 @@ module BSV
13
13
  autoload :Registry, 'bsv/registry'
14
14
  autoload :MCP, 'bsv/mcp'
15
15
 
16
- autoload :WireFormat, 'bsv/wire_format'
16
+ # Wallet is loaded eagerly to avoid load-path shadowing when bsv-wallet is
17
+ # also in the bundle (bsv-wallet/lib/bsv/wallet.rb would otherwise win).
18
+ require_relative 'bsv/wallet'
19
+ autoload :WireFormat, 'bsv/wire_format'
17
20
  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.14.0
4
+ version: 0.16.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
@@ -23,6 +37,20 @@ dependencies:
23
37
  - - "~>"
24
38
  - !ruby/object:Gem::Version
25
39
  version: '0.12'
40
+ - !ruby/object:Gem::Dependency
41
+ name: secp256k1-native
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0.16'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0.16'
26
54
  description: A Ruby library for interacting with the BSV Blockchain — keys, scripts,
27
55
  transactions, and more.
28
56
  executables:
@@ -142,6 +170,7 @@ files:
142
170
  - lib/bsv/script/opcodes.rb
143
171
  - lib/bsv/script/push_drop_template.rb
144
172
  - lib/bsv/script/script.rb
173
+ - lib/bsv/secp256k1_native.bundle
145
174
  - lib/bsv/transaction.rb
146
175
  - lib/bsv/transaction/beef.rb
147
176
  - lib/bsv/transaction/chain_tracker.rb
@@ -162,6 +191,13 @@ files:
162
191
  - lib/bsv/transaction/var_int.rb
163
192
  - lib/bsv/transaction/verification_error.rb
164
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
165
201
  - lib/bsv/wire_format.rb
166
202
  homepage: https://github.com/sgbett/bsv-ruby-sdk
167
203
  licenses: