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
@@ -49,8 +49,17 @@ module BSV
49
49
 
50
50
  def self.call(txid:, network: nil, server_context: nil)
51
51
  net_sym = Helpers.resolve_network_sym(network, server_context)
52
- woc = BSV::Network::WhatsOnChain.new(network: net_sym)
53
- tx = woc.fetch_transaction(txid)
52
+ provider = BSV::Network::Providers::WhatsOnChain.default(network: net_sym)
53
+ fetch_result = provider.call(:get_tx, txid)
54
+
55
+ unless fetch_result.success?
56
+ code = fetch_result.metadata[:status_code]
57
+ msg = fetch_result.message
58
+ msg = "#{msg} (HTTP #{code})" if code
59
+ return Helpers.error_response(msg)
60
+ end
61
+
62
+ tx = BSV::Transaction::Transaction.from_hex(fetch_result.data)
54
63
 
55
64
  result = {
56
65
  hex: tx.to_hex,
@@ -61,8 +70,6 @@ module BSV
61
70
  [::MCP::Content::Text.new(result.to_json)],
62
71
  structured_content: result
63
72
  )
64
- rescue BSV::Network::ChainProviderError => e
65
- Helpers.error_response("#{e.message} (HTTP #{e.status_code})")
66
73
  rescue ArgumentError => e
67
74
  Helpers.error_response(e.message)
68
75
  end
@@ -50,8 +50,22 @@ module BSV
50
50
 
51
51
  def self.call(address:, network: nil, server_context: nil)
52
52
  net_sym = Helpers.resolve_network_sym(network, server_context)
53
- woc = BSV::Network::WhatsOnChain.new(network: net_sym)
54
- utxos = woc.fetch_utxos(address)
53
+ provider = BSV::Network::Providers::WhatsOnChain.default(network: net_sym)
54
+ utxo_result = provider.call(:get_utxos_all, address)
55
+
56
+ unless utxo_result.success?
57
+ code = utxo_result.metadata[:status_code]
58
+ msg = utxo_result.message
59
+ msg = "#{msg} (HTTP #{code})" if code
60
+ return Helpers.error_response(msg)
61
+ end
62
+
63
+ utxos = utxo_result.data.map do |entry|
64
+ BSV::Network::UTXO.new(
65
+ tx_hash: entry[:tx_hash], tx_pos: entry[:tx_pos],
66
+ satoshis: entry[:satoshis], height: entry[:height]
67
+ )
68
+ end
55
69
 
56
70
  result = {
57
71
  address: address,
@@ -63,8 +77,6 @@ module BSV
63
77
  [::MCP::Content::Text.new(result.to_json)],
64
78
  structured_content: result
65
79
  )
66
- rescue BSV::Network::ChainProviderError => e
67
- Helpers.error_response("#{e.message} (HTTP #{e.status_code})")
68
80
  end
69
81
  end
70
82
  end
@@ -2,164 +2,24 @@
2
2
 
3
3
  module BSV
4
4
  module Network
5
- # ARC broadcaster for submitting transactions to the BSV network.
6
- #
7
- # This facade preserves the legacy public API contract while delegating all
8
- # HTTP logic to +Protocols::ARC+. It translates +Result+ objects returned by
9
- # the protocol into +BroadcastResponse+ instances or raised +BroadcastError+
10
- # exceptions as required by consumer code.
11
- #
12
- # Any object responding to #broadcast(tx) can serve as a broadcaster;
13
- # this class implements that contract using the ARC API.
5
+ # @deprecated Use {BSV::Network::Protocols::ARC} directly instead.
6
+ # The facade converted clean Result objects into exceptions — every
7
+ # consumer immediately caught them and converted back to data.
8
+ # Use the protocol layer, which returns Result objects natively.
14
9
  class ARC
15
- # Returns an ARC instance pointed at the GorillaPool public ARC endpoint.
16
- #
17
- # @param testnet [Boolean] when true, uses the GorillaPool testnet endpoint
18
- # @param api_key [String, nil] optional bearer token for Authorization
19
- # @param http_client [#request, nil] injectable HTTP client for testing
20
- # @param opts [Hash] ARC-specific options forwarded to the protocol
21
- # (e.g. +deployment_id:+, +callback_url:+, +callback_token:+)
22
- # @return [ARC]
23
- def self.default(testnet: false, api_key: nil, http_client: nil, **opts)
24
- provider = Providers::GorillaPool.default(testnet: testnet)
25
- arc_protocol = provider.protocol_for(:broadcast)
26
- base_url = arc_protocol.base_url
27
- new(
28
- base_url,
29
- api_key: api_key,
30
- http_client: http_client,
31
- **opts
32
- )
33
- end
34
-
35
- # ARC response statuses that indicate the transaction was NOT accepted.
36
- REJECTED_STATUSES = %w[
37
- REJECTED
38
- DOUBLE_SPEND_ATTEMPTED
39
- INVALID
40
- MALFORMED
41
- MINED_IN_STALE_BLOCK
42
- ].freeze
43
-
44
- # @param url_or_protocol [String, Protocols::ARC] ARC base URL (without
45
- # trailing slash) or a pre-configured +Protocols::ARC+ instance. When a
46
- # URL string is supplied, the remaining keyword arguments are forwarded to
47
- # the underlying protocol constructor. When a protocol instance is supplied,
48
- # all keyword arguments are ignored.
49
- # @param api_key [String, nil] optional bearer token for Authorization
50
- # @param deployment_id [String, nil] optional deployment identifier for
51
- # the +XDeployment-ID+ header; defaults to a per-instance random value
52
- # @param callback_url [String, nil] optional +X-CallbackUrl+ for ARC
53
- # status callbacks
54
- # @param callback_token [String, nil] optional +X-CallbackToken+ for
55
- # ARC status callback authentication
56
- # @param http_client [#request, nil] injectable HTTP client for testing
57
- def initialize(url_or_protocol, api_key: nil, deployment_id: nil, callback_url: nil,
58
- callback_token: nil, http_client: nil)
59
- @protocol =
60
- if url_or_protocol.is_a?(String)
61
- Protocols::ARC.new(
62
- base_url: url_or_protocol,
63
- api_key: api_key,
64
- deployment_id: deployment_id,
65
- callback_url: callback_url,
66
- callback_token: callback_token,
67
- http_client: http_client
68
- )
69
- else
70
- url_or_protocol
71
- end
72
- end
73
-
74
- # Submit a transaction to ARC.
75
- #
76
- # @param tx [Transaction] the transaction to broadcast
77
- # @param wait_for [String, nil] ARC wait condition
78
- # @param skip_fee_validation [Boolean, nil] when truthy, sends X-SkipFeeValidation header
79
- # @param skip_script_validation [Boolean, nil] when truthy, sends X-SkipScriptValidation header
80
- # @return [BroadcastResponse]
81
- # @raise [BroadcastError] when ARC returns a non-2xx HTTP status or a
82
- # rejected/orphan +txStatus+
83
- def broadcast(tx, wait_for: nil, skip_fee_validation: nil, skip_script_validation: nil)
84
- result = @protocol.call(
85
- :broadcast, tx,
86
- wait_for: wait_for,
87
- skip_fee_validation: skip_fee_validation,
88
- skip_script_validation: skip_script_validation
89
- )
90
- result_to_response!(result)
91
- end
92
-
93
- # Submit multiple transactions to ARC in a single batch request.
94
- #
95
- # Returns a mixed array of {BroadcastResponse} and {BroadcastError} objects.
96
- # Per-transaction rejections are returned as {BroadcastError} values rather
97
- # than raised. Only HTTP-level errors raise a {BroadcastError} for the whole batch.
98
- #
99
- # @param txs [Array<Transaction>] transactions to broadcast
100
- # @param wait_for [String, nil] ARC wait condition
101
- # @param skip_fee_validation [Boolean, nil]
102
- # @param skip_script_validation [Boolean, nil]
103
- # @return [Array<BroadcastResponse, BroadcastError>]
104
- # @raise [BroadcastError] when ARC returns a non-2xx HTTP status
105
- def broadcast_many(txs, wait_for: nil, skip_fee_validation: nil, skip_script_validation: nil)
106
- result = @protocol.call(
107
- :broadcast_many, txs,
108
- wait_for: wait_for,
109
- skip_fee_validation: skip_fee_validation,
110
- skip_script_validation: skip_script_validation
111
- )
10
+ # Raised when deprecated facade classes are instantiated.
11
+ class DeprecationError < StandardError; end
112
12
 
113
- case result
114
- when Result::Success
115
- result.data.map { |item| item_to_response_or_error(item) }
116
- else
117
- raise broadcast_error_from_result(result)
118
- end
119
- end
120
-
121
- # Query the status of a previously submitted transaction.
122
- #
123
- # @param txid [String] transaction ID to query
124
- # @return [BroadcastResponse]
125
- # @raise [BroadcastError] on failure
126
- def status(txid)
127
- result = @protocol.call(:get_tx_status, txid)
128
- result_to_response!(result)
129
- end
130
-
131
- private
132
-
133
- # Translate a single-tx Result into a BroadcastResponse or raise BroadcastError.
134
- def result_to_response!(result)
135
- case result
136
- when Result::Success
137
- BroadcastResponse.new(result.data)
138
- else
139
- raise broadcast_error_from_result(result)
140
- end
141
- end
13
+ MESSAGE = 'BSV::Network::ARC is deprecated. ' \
14
+ 'Use BSV::Network::Protocols::ARC directly — it returns Result objects ' \
15
+ 'instead of raising exceptions. See BSV::Network::Protocols::ARC for usage.'
142
16
 
143
- # Translate a per-item batch Result into a BroadcastResponse or BroadcastError
144
- # (returned, not raised).
145
- def item_to_response_or_error(item)
146
- case item
147
- when Result::Success
148
- BroadcastResponse.new(item.data)
149
- else
150
- broadcast_error_from_result(item)
151
- end
17
+ def self.default(**)
18
+ raise DeprecationError, MESSAGE
152
19
  end
153
20
 
154
- # Build a BroadcastError from a Result::Error or Result::NotFound.
155
- def broadcast_error_from_result(result)
156
- meta = result.metadata || {}
157
- BroadcastError.new(
158
- result.message,
159
- status_code: meta[:status_code],
160
- txid: meta[:txid],
161
- arc_status: meta[:arc_status]
162
- )
21
+ def initialize(*)
22
+ raise DeprecationError, MESSAGE
163
23
  end
164
24
  end
165
25
  end
@@ -2,118 +2,24 @@
2
2
 
3
3
  module BSV
4
4
  module Network
5
- # WhatsOnChain chain data provider for reading transactions and UTXOs
6
- # from the BSV network.
7
- #
8
- # Any object responding to #fetch_utxos(address),
9
- # #fetch_transaction(txid), #current_height,
10
- # #get_block_header(height), and optionally
11
- # #valid_root_for_height?(root_hex, height) can serve as a chain
12
- # data source;
13
- # this class implements that contract by delegating to
14
- # Protocols::WoCREST.
15
- #
16
- # The HTTP client is injectable for testability. It must respond to
17
- # #request(uri, request) and return an object with #code and #body.
5
+ # @deprecated Use {BSV::Network::Protocols::WoCREST} directly instead.
6
+ # The facade converted clean Result objects into exceptions — every
7
+ # consumer immediately caught them and converted back to data.
8
+ # Use the protocol layer, which returns Result objects natively.
18
9
  class WhatsOnChain
19
- # Returns a WhatsOnChain instance using the provider default.
20
- #
21
- # @param testnet [Boolean] when true, uses the testnet endpoint
22
- # @param opts [Hash] forwarded to the underlying protocol (e.g. +api_key:+, +http_client:+)
23
- # @return [WhatsOnChain]
24
- def self.default(testnet: false, **opts)
25
- provider = Providers::WhatsOnChain.default(testnet: testnet, **opts)
26
- new(protocol: provider.protocol_for(:get_tx))
27
- end
28
-
29
- # @param network [Symbol] :main, :mainnet, :test, :testnet, :stn (legacy compat)
30
- # @param http_client [#request, nil] injectable HTTP client
31
- # @param protocol [BSV::Network::Protocols::WoCREST, nil] pre-configured protocol
32
- def initialize(network: :mainnet, http_client: nil, protocol: nil)
33
- if protocol
34
- @protocol = protocol
35
- else
36
- provider = Providers::WhatsOnChain.default(network: network, http_client: http_client)
37
- @protocol = provider.protocol_for(:get_tx)
38
- end
39
- end
40
-
41
- # Fetch unspent transaction outputs for an address.
42
- # @param address [String] BSV address
43
- # @return [Array<UTXO>]
44
- def fetch_utxos(address)
45
- result = @protocol.call(:get_utxos_all, address)
46
- raise_on_error(result)
47
-
48
- result.data.map do |entry|
49
- UTXO.new(
50
- tx_hash: entry[:tx_hash],
51
- tx_pos: entry[:tx_pos],
52
- satoshis: entry[:satoshis],
53
- height: entry[:height]
54
- )
55
- end
56
- end
57
-
58
- # Fetch a raw transaction by its txid and parse it.
59
- # @param txid [String] transaction ID (hex)
60
- # @return [BSV::Transaction::Transaction]
61
- def fetch_transaction(txid)
62
- result = @protocol.call(:get_tx, txid)
63
- raise_on_error(result)
64
-
65
- BSV::Transaction::Transaction.from_hex(result.data)
66
- end
10
+ # Raised when deprecated facade classes are instantiated.
11
+ class DeprecationError < StandardError; end
67
12
 
68
- # Return the current blockchain height.
69
- # @return [Integer]
70
- # @raise [BSV::Network::ChainProviderError] on network or API error
71
- def current_height
72
- result = @protocol.call(:current_height)
73
- raise_on_error(result)
13
+ MESSAGE = 'BSV::Network::WhatsOnChain is deprecated. ' \
14
+ 'Use BSV::Network::Protocols::WoCREST directly — it returns Result objects ' \
15
+ 'instead of raising exceptions. See BSV::Network::Protocols::WoCREST for usage.'
74
16
 
75
- result.data
17
+ def self.default(**)
18
+ raise DeprecationError, MESSAGE
76
19
  end
77
20
 
78
- # Fetch the block header for a given height.
79
- # @param height [Integer] block height
80
- # @return [Hash] parsed block header JSON
81
- # @raise [BSV::Network::ChainProviderError] on network or API error
82
- def get_block_header(height)
83
- result = @protocol.call(:get_block_header, height)
84
- raise_on_error(result)
85
-
86
- result.data
87
- end
88
-
89
- # Verify that a merkle root is valid for the given block height.
90
- # Returns +false+ when the block is not found (404); raises on other errors.
91
- # @param root [String] expected merkle root as hex
92
- # @param height [Integer] block height
93
- # @return [Boolean]
94
- # @raise [BSV::Network::ChainProviderError] on network or non-404 API error
95
- def valid_root_for_height?(root, height)
96
- result = @protocol.call(:valid_root, root, height)
97
- return false if result.not_found?
98
-
99
- raise_on_error(result)
100
-
101
- result.data == true
102
- end
103
-
104
- private
105
-
106
- # Translates a non-success Protocol result into a raised ChainProviderError.
107
- #
108
- # @param result [Result::Error, Result::NotFound]
109
- # @raise [ChainProviderError]
110
- def raise_on_error(result)
111
- return if result.success?
112
-
113
- raise ChainProviderError.new(
114
- result.message || 'Request failed',
115
- status_code: result.metadata[:status_code]
116
- )
21
+ def initialize(*)
22
+ raise DeprecationError, MESSAGE
117
23
  end
118
24
  end
119
25
  end
@@ -90,7 +90,7 @@ module BSV
90
90
 
91
91
  sig_args = { hash_to_directly_sign: hash_bytes, protocol_id: @protocol_id, key_id: '1', counterparty: 'self' }
92
92
  sig_args[:originator] = @originator if @originator
93
- result = @wallet.create_signature(sig_args)
93
+ result = @wallet.create_signature(**sig_args)
94
94
 
95
95
  sig_bytes = result[:signature].pack('C*')
96
96
  sig_with_hashtype = sig_bytes + [sighash_type].pack('C')
@@ -171,14 +171,14 @@ module BSV
171
171
  # Fetch the wallet's identity key (compressed public key hex)
172
172
  id_args = { identity_key: true }
173
173
  id_args[:originator] = @originator if @originator
174
- identity_result = @wallet.get_public_key(id_args)
174
+ identity_result = @wallet.get_public_key(**id_args)
175
175
  identity_key_hex = identity_result[:public_key]
176
176
  identity_key_bytes = [identity_key_hex].pack('H*')
177
177
 
178
178
  # Derive the locking public key for this protocol
179
179
  lock_args = { protocol_id: protocol_id, key_id: '1', counterparty: 'self' }
180
180
  lock_args[:originator] = @originator if @originator
181
- locking_result = @wallet.get_public_key(lock_args)
181
+ locking_result = @wallet.get_public_key(**lock_args)
182
182
  locking_pubkey_hex = locking_result[:public_key]
183
183
  locking_pubkey_bytes = [locking_pubkey_hex].pack('H*')
184
184
 
@@ -192,7 +192,7 @@ module BSV
192
192
  data_to_sign = (field_protocol + field_identity + field_domain + field_topic).unpack('C*')
193
193
  sig_args = { data: data_to_sign, protocol_id: protocol_id, key_id: '1', counterparty: 'self' }
194
194
  sig_args[:originator] = @originator if @originator
195
- sig_result = @wallet.create_signature(sig_args)
195
+ sig_result = @wallet.create_signature(**sig_args)
196
196
  field_sig = sig_result[:signature].pack('C*')
197
197
 
198
198
  fields = [field_protocol, field_identity, field_domain, field_topic, field_sig]
@@ -42,6 +42,7 @@ module BSV
42
42
  bytes.each_byte { |b| b.zero? ? leading_zeros += 1 : break }
43
43
 
44
44
  # Convert to big integer and repeatedly divide by 58
45
+ # C-backed hex conversion — 10x faster than pure-Ruby byte shifting; not a porting artefact.
45
46
  n = bytes.unpack1('H*').to_i(16)
46
47
  result = +''
47
48
  while n.positive?
@@ -78,7 +79,7 @@ module BSV
78
79
  n = (n * BASE) + digit
79
80
  end
80
81
 
81
- # Convert integer to bytes
82
+ # Convert integer to bytes — C-backed hex round-trip is the fastest pure-Ruby integer→bytes path.
82
83
  hex = n.zero? ? '' : n.to_s(16)
83
84
  hex = "0#{hex}" if hex.length.odd?
84
85
  result = [hex].pack('H*')
@@ -25,10 +25,11 @@ module BSV
25
25
 
26
26
  module_function
27
27
 
28
- # Multiply the generator point by a scalar (variable-time, wNAF).
28
+ # Multiply the generator point by a scalar (constant-time).
29
29
  #
30
- # Suitable for public scalars only (e.g. verify paths). For secret
31
- # scalars use {multiply_generator_ct}.
30
+ # Uses the Montgomery ladder by default, matching OpenSSL convention.
31
+ # Safe for both secret and public scalars. For explicit variable-time
32
+ # multiplication of public scalars, use {multiply_generator_vt}.
32
33
  #
33
34
  # @param scalar_bn [OpenSSL::BN] the scalar multiplier
34
35
  # @return [OpenSSL::PKey::EC::Point] the resulting curve point
@@ -38,19 +39,31 @@ module BSV
38
39
 
39
40
  # Multiply the generator point by a secret scalar (constant-time).
40
41
  #
41
- # Uses the Montgomery ladder to avoid timing side-channels on the
42
- # scalar. Use for key generation and signing.
42
+ # Alias for {multiply_generator} retained for backward compatibility
43
+ # and expressiveness.
43
44
  #
44
45
  # @param scalar_bn [OpenSSL::BN] the secret scalar multiplier
45
46
  # @return [OpenSSL::PKey::EC::Point] the resulting curve point
46
47
  def multiply_generator_ct(scalar_bn)
47
- G.mul_ct(scalar_bn)
48
+ G.mul(scalar_bn)
48
49
  end
49
50
 
50
- # Multiply an arbitrary curve point by a scalar (variable-time, wNAF).
51
+ # Multiply the generator point by a public scalar (variable-time, wNAF).
51
52
  #
52
- # Suitable for public scalars only. For secret scalars use
53
- # {multiply_point_ct}.
53
+ # Faster than {multiply_generator} but leaks timing information about
54
+ # the scalar. Use only for public scalars (e.g. signature verification).
55
+ #
56
+ # @param scalar_bn [OpenSSL::BN] the public scalar multiplier
57
+ # @return [OpenSSL::PKey::EC::Point] the resulting curve point
58
+ def multiply_generator_vt(scalar_bn)
59
+ G.mul_vt(scalar_bn)
60
+ end
61
+
62
+ # Multiply an arbitrary curve point by a scalar (constant-time).
63
+ #
64
+ # Uses the Montgomery ladder by default, matching OpenSSL convention.
65
+ # Safe for both secret and public scalars. For explicit variable-time
66
+ # multiplication of public scalars, use {multiply_point_vt}.
54
67
  #
55
68
  # @param point [OpenSSL::PKey::EC::Point] the point to multiply
56
69
  # @param scalar_bn [OpenSSL::BN] the scalar multiplier
@@ -61,14 +74,26 @@ module BSV
61
74
 
62
75
  # Multiply an arbitrary curve point by a secret scalar (constant-time).
63
76
  #
64
- # Uses the Montgomery ladder to avoid timing side-channels on the
65
- # scalar. Use for ECDH shared-secret derivation.
77
+ # Alias for {multiply_point} retained for backward compatibility
78
+ # and expressiveness.
66
79
  #
67
80
  # @param point [OpenSSL::PKey::EC::Point] the base point
68
81
  # @param scalar_bn [OpenSSL::BN] the secret scalar multiplier
69
82
  # @return [OpenSSL::PKey::EC::Point] the resulting curve point
70
83
  def multiply_point_ct(point, scalar_bn)
71
- point.mul_ct(scalar_bn)
84
+ point.mul(scalar_bn)
85
+ end
86
+
87
+ # Multiply an arbitrary curve point by a public scalar (variable-time, wNAF).
88
+ #
89
+ # Faster than {multiply_point} but leaks timing information about
90
+ # the scalar. Use only for public scalars (e.g. signature verification).
91
+ #
92
+ # @param point [OpenSSL::PKey::EC::Point] the point to multiply
93
+ # @param scalar_bn [OpenSSL::BN] the public scalar multiplier
94
+ # @return [OpenSSL::PKey::EC::Point] the resulting curve point
95
+ def multiply_point_vt(point, scalar_bn)
96
+ point.mul_vt(scalar_bn)
72
97
  end
73
98
 
74
99
  # Add two curve points together.
@@ -82,8 +82,8 @@ module BSV
82
82
  u1 = ((n - e) * r_inv) % n
83
83
  u2 = (s * r_inv) % n
84
84
 
85
- p1 = Curve.multiply_generator(u1)
86
- p2 = Curve.multiply_point(r_point, u2)
85
+ p1 = Curve.multiply_generator_vt(u1)
86
+ p2 = Curve.multiply_point_vt(r_point, u2)
87
87
  q = Curve.add_points(p1, p2)
88
88
 
89
89
  raise ArgumentError, 'recovered point is at infinity' if q.infinity?
@@ -112,8 +112,8 @@ module BSV
112
112
  u2 = (r * s_inv) % n
113
113
 
114
114
  # R' = u1*G + u2*Q
115
- point1 = Curve.multiply_generator(u1)
116
- point2 = Curve.multiply_point(public_key_point, u2)
115
+ point1 = Curve.multiply_generator_vt(u1)
116
+ point2 = Curve.multiply_point_vt(public_key_point, u2)
117
117
  result_point = Curve.add_points(point1, point2)
118
118
 
119
119
  return false if result_point.infinity?
@@ -66,6 +66,22 @@ class BSVShimECPoint
66
66
  pt
67
67
  end
68
68
 
69
+ # Scalar multiplication: self * scalar (constant-time, Montgomery ladder).
70
+ #
71
+ # Matches OpenSSL convention where +EC_POINT_mul+ is always constant-time.
72
+ # Safe for both secret and public scalars.
73
+ #
74
+ # Also supports the multi-scalar form: +mul(bns, points)+ computes
75
+ # <tt>bns[0]*self + bns[1]*points[0] + ...</tt>
76
+ # where +bns.length == points.length + 1+.
77
+ #
78
+ # @overload mul(scalar_bn)
79
+ # @param scalar_bn [OpenSSL::BN, Integer] the scalar multiplier
80
+ # @overload mul(bns, points)
81
+ # @param bns [Array<OpenSSL::BN>] scalars; must have +points.length + 1+ elements
82
+ # @param points [Array<BSVShimECPoint>] additional points
83
+ # @raise [NoMethodError] if +bns+ and +points+ lengths are mismatched
84
+ # @return [BSVShimECPoint]
69
85
  def mul(*args)
70
86
  if args.length == 1
71
87
  scalar = bn_to_int(args[0])
@@ -85,16 +101,27 @@ class BSVShimECPoint
85
101
  end
86
102
  end
87
103
 
88
- # Constant-time scalar multiplication via the Montgomery ladder.
104
+ # Constant-time scalar multiplication (alias for {#mul}).
89
105
  #
90
- # Delegates to {BSV::Primitives::Secp256k1::Point#mul_ct} to ensure
91
- # secret-scalar paths execute in constant time.
106
+ # Retained for backward compatibility and expressiveness. Delegates
107
+ # to {#mul}, which is constant-time by default.
92
108
  #
93
- # @param scalar_bn [OpenSSL::BN, Integer] the secret scalar
109
+ # @param scalar_bn [OpenSSL::BN, Integer] the scalar multiplier
94
110
  # @return [BSVShimECPoint]
95
111
  def mul_ct(scalar_bn)
112
+ mul(scalar_bn)
113
+ end
114
+
115
+ # Variable-time scalar multiplication (wNAF).
116
+ #
117
+ # Faster than {#mul} but leaks timing information about the scalar.
118
+ # Use only for public scalars (e.g. signature verification).
119
+ #
120
+ # @param scalar_bn [OpenSSL::BN, Integer] the public scalar multiplier
121
+ # @return [BSVShimECPoint]
122
+ def mul_vt(scalar_bn)
96
123
  scalar = bn_to_int(scalar_bn)
97
- result = @secp_point.mul_ct(scalar)
124
+ result = @secp_point.mul_vt(scalar)
98
125
  self.class.from_secp_point(@group, result)
99
126
  end
100
127
 
@@ -165,7 +165,7 @@ module BSV
165
165
  def derive_child(public_key, invoice_number)
166
166
  shared = derive_shared_secret(public_key)
167
167
  hmac = Digest.hmac_sha256(shared.compressed, invoice_number.encode('UTF-8'))
168
- hmac_bn = OpenSSL::BN.new(hmac.unpack1('H*'), 16)
168
+ hmac_bn = OpenSSL::BN.new(hmac, 2)
169
169
  PrivateKey.new(@bn.mod_add(hmac_bn, Curve::N))
170
170
  end
171
171
 
@@ -207,7 +207,7 @@ module BSV
207
207
  loop do
208
208
  counter_bytes = [i, attempts].pack('N*') + SecureRandom.random_bytes(32)
209
209
  h = Digest.hmac_sha512(seed, counter_bytes)
210
- candidate = OpenSSL::BN.new(h.unpack1('H*'), 16) % PointInFiniteField::P
210
+ candidate = OpenSSL::BN.new(h, 2) % PointInFiniteField::P
211
211
 
212
212
  attempts += 1
213
213
  raise ArgumentError, 'failed to generate unique x-coordinate after 5 attempts' if attempts > 5
@@ -132,7 +132,7 @@ module BSV
132
132
  def derive_child(private_key, invoice_number)
133
133
  shared = derive_shared_secret(private_key)
134
134
  hmac = Digest.hmac_sha256(shared.compressed, invoice_number.encode('UTF-8'))
135
- hmac_bn = OpenSSL::BN.new(hmac.unpack1('H*'), 16)
135
+ hmac_bn = OpenSSL::BN.new(hmac, 2)
136
136
  hmac_point = Curve.multiply_generator_ct(hmac_bn)
137
137
  child_point = Curve.add_points(@point, hmac_point)
138
138
  PublicKey.new(child_point)
@@ -97,15 +97,15 @@ module BSV
97
97
  e = compute_challenge(public_key_a, public_key_b, shared_secret, proof.s_prime, proof.r)
98
98
 
99
99
  # Equation 1: z·G == R + e·A
100
- z_g = Curve.multiply_generator(proof.z)
101
- e_a = Curve.multiply_point(public_key_a.point, e)
100
+ z_g = Curve.multiply_generator_vt(proof.z)
101
+ e_a = Curve.multiply_point_vt(public_key_a.point, e)
102
102
  r_plus_ea = Curve.add_points(proof.r.point, e_a)
103
103
 
104
104
  return false unless points_equal?(z_g, r_plus_ea)
105
105
 
106
106
  # Equation 2: z·B == S' + e·S
107
- z_b = Curve.multiply_point(public_key_b.point, proof.z)
108
- e_s = Curve.multiply_point(shared_secret.point, e)
107
+ z_b = Curve.multiply_point_vt(public_key_b.point, proof.z)
108
+ e_s = Curve.multiply_point_vt(shared_secret.point, e)
109
109
  sp_plus_es = Curve.add_points(proof.s_prime.point, e_s)
110
110
 
111
111
  points_equal?(z_b, sp_plus_es)