bsv-sdk 0.3.2 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9924711798c6f50752254b528fb1956bcc39e3cf5cf1b57fdf94a887a3dc9688
4
- data.tar.gz: d3cd183e1ccdafadbe37b989275cd03d4c505e2432b3ede6d1af5213b7455361
3
+ metadata.gz: 5d32b117209fc6509e5020811725c5b5bb5e776c57fbd0f3cb0afa8cf3cc12ca
4
+ data.tar.gz: 2fa913c8e3fe270b53a1748e2ff4bdb8055945b625a9e3075eb293b78d581fe4
5
5
  SHA512:
6
- metadata.gz: 88f3ed7869e81aec560b47a405e9144d00e3e48cf4b62d5be6bd09e58ac216ebbe8b56ebcff2ac61075c901d56308875b7ff4673d2e066df74f723e6391f3ea5
7
- data.tar.gz: a88d52282aa5d2a05b906685245b6690c434dbea73fb01ed99be1dc93a4706720c13ee5fb48830a4c3c423c9160db264fe66c910de5995c94047f61e7f56120d
6
+ metadata.gz: 82e8ca0cf9e266d7c80fe8be42be40450e5a3495db083f90aa098aaf7ff0186ecc4a63b3a1efd9b7447738d857b930f655be291ad098a83ef7a6d0095df0b4d0
7
+ data.tar.gz: 103b3246361a57c3b1161c789c097c147b6c7070781094587f59bdee6caf3141bb5b22934a89e1bfdd4e514b4f45eb2edd1ae218fc92f23e2d520a3a56f72113
data/CHANGELOG.md CHANGED
@@ -5,6 +5,71 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-04-01
9
+
10
+ ### Added
11
+
12
+ #### Primitives
13
+
14
+ - **Bitcore ECIES** — `ECIES.bitcore_encrypt` / `ECIES.bitcore_decrypt`. AES-256-CBC with random IV, SHA-512(X-coordinate) key derivation. Matches ts-sdk and go-sdk Bitcore variants.
15
+
16
+ #### Transaction
17
+
18
+ - **LivePolicy.default** — one-line convenience for live fee queries via GorillaPool ARC with 5-minute cache and 100 sat/kB fallback.
19
+
20
+ ### Changed
21
+
22
+ - **Default fee rate**: `SatoshisPerKilobyte` default changed from 50 to 100 sat/kB (matches ts-sdk LivePolicy fallback). `Wallet#fund` default changed from 0.5 to 0.1 sat/byte.
23
+
24
+ ### bsv-wallet v0.2.0
25
+
26
+ - **FileStore** — JSON file-backed persistent storage, now the default for `WalletClient`. Data survives process restarts. MemoryStore becomes explicit opt-in for tests.
27
+ - **File permissions** — directory created with 0700, files with 0600. Warns via Logger on startup if permissions are too open.
28
+ - **BRC-31 Auth/Peer** — mutual authentication with nonce-based challenges, ECDSA signatures, and session management.
29
+ - **Wire protocol** — binary ABI serialisation for all 28 BRC-100 methods (call codes 1-28, VarInt encoding).
30
+ - **Certificate issuance** — `acquire_certificate` with `'issuance'` protocol (POST to certifier URL).
31
+ - **OpCat template** — OP_CAT concatenation script template with lock/unlock constructors.
32
+ - **Live fee policy** — `LivePolicy` fee model fetching from ARC `/v1/policy`.
33
+
34
+ ### Fixed
35
+
36
+ - Subject and certifier pinned in certificate issuance response (not overridable by remote certifier)
37
+ - Wire reader negative privileged_reason length crash
38
+ - PUSHDATA1/2/4 bounds check (silent data corruption on truncated scripts)
39
+ - Extended key path validation (reject non-numeric indices)
40
+
41
+ ## [0.3.0] - 2026-03-27
42
+
43
+ ### Added
44
+
45
+ #### Primitives
46
+
47
+ - **SymmetricKey** — AES-256-GCM encryption/decryption with 32-byte IV (cross-SDK compatible). Construct from random, ECDH, or raw bytes.
48
+ - **BRC-77 SignedMessage** — authenticated message signing and verification using BRC-42 derived keys. Supports targeted (specific verifier) and "anyone" modes.
49
+ - **BRC-78 EncryptedMessage** — end-to-end encrypted messaging using ECDH-derived symmetric keys.
50
+ - **Schnorr ZKP (BRC-94)** — zero-knowledge proof of ECDH shared secret knowledge. `Schnorr.generate_proof` / `Schnorr.verify_proof`.
51
+ - **Shamir's Secret Sharing** — split private keys into threshold shares (`PrivateKey#to_key_shares`) with Lagrange interpolation reconstruction. Backup format with integrity check.
52
+
53
+ #### Script
54
+
55
+ - **PushDrop template** — data carrier with P2PK spending. `Script.pushdrop_lock` / `Script.pushdrop_unlock` with field extraction.
56
+ - **RPuzzle template** — R-puzzle hash-based spending with 6 hash type variants (raw, SHA1, SHA256, RIPEMD160, HASH160, HASH256).
57
+
58
+ #### Transaction
59
+
60
+ - **Benford's law change distribution** — privacy-preserving change output splitting using Benford's first-digit distribution.
61
+ - **ARC X-WaitFor** — `ARC#broadcast` gains `wait_for:` parameter for `X-WaitFor` header (RECEIVED, STORED, ANNOUNCED_TO_NETWORK, SEEN_ON_NETWORK, MINED).
62
+
63
+ ### Fixed
64
+
65
+ - Empty plaintext/ciphertext handling on older OpenSSL versions
66
+ - PushDrop detection for minimally-encoded fields
67
+
68
+ ### Changed
69
+
70
+ - `Transaction#fee` change distribution uses Benford's law (was equal split)
71
+ - `LineLength` raised to 150
72
+
8
73
  ## [0.2.1] - 2026-03-07
9
74
 
10
75
  ### Fixed
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'openssl'
4
+ require 'securerandom'
4
5
 
5
6
  module BSV
6
7
  module Primitives
@@ -95,6 +96,74 @@ module BSV
95
96
  end
96
97
  end
97
98
 
99
+ # Encrypt a message using the Bitcore ECIES variant.
100
+ #
101
+ # Differs from the Electrum variant: no magic prefix, AES-256-CBC
102
+ # (not AES-128), random IV prepended to ciphertext, and HMAC covers
103
+ # the ciphertext (not the ephemeral pubkey).
104
+ #
105
+ # Wire format: +ephemeral_pub(33) + IV(16) + ciphertext + HMAC(32)+
106
+ #
107
+ # @param message [String] the plaintext message
108
+ # @param public_key [PublicKey] the recipient's public key
109
+ # @param private_key [PrivateKey, nil] optional ephemeral key (random if omitted)
110
+ # @return [String] encrypted payload
111
+ def bitcore_encrypt(message, public_key, private_key: nil)
112
+ message = message.b if message.encoding != Encoding::ASCII_8BIT
113
+
114
+ ephemeral = private_key || PrivateKey.generate
115
+ key_e, key_m = derive_bitcore_keys(ephemeral, public_key)
116
+
117
+ iv = SecureRandom.random_bytes(16)
118
+
119
+ cipher = OpenSSL::Cipher.new('aes-256-cbc')
120
+ cipher.encrypt
121
+ cipher.key = key_e
122
+ cipher.iv = iv
123
+ ciphertext = message.empty? ? cipher.final : cipher.update(message) + cipher.final
124
+
125
+ c = iv + ciphertext
126
+ mac = Digest.hmac_sha256(key_m, c)
127
+
128
+ ephemeral.public_key.compressed + c + mac
129
+ end
130
+
131
+ # Decrypt a message encrypted with the Bitcore ECIES variant.
132
+ #
133
+ # @param data [String] the encrypted payload (Bitcore ECIES format)
134
+ # @param private_key [PrivateKey] the recipient's private key
135
+ # @return [String] the decrypted plaintext
136
+ # @raise [ArgumentError] if the data is too short
137
+ # @raise [DecryptionError] if HMAC verification or AES decryption fails
138
+ def bitcore_decrypt(data, private_key)
139
+ data = data.b if data.encoding != Encoding::ASCII_8BIT
140
+
141
+ # Minimum: ephemeral_pub(33) + IV(16) + AES block(16) + HMAC(32) = 97
142
+ raise ArgumentError, 'data too short' if data.bytesize < 97
143
+
144
+ ephemeral_pub = PublicKey.from_bytes(data[0, 33])
145
+ mac = data[-32, 32]
146
+ c = data[33...-32] # IV + ciphertext
147
+
148
+ key_e, key_m = derive_bitcore_keys(private_key, ephemeral_pub)
149
+
150
+ expected_mac = Digest.hmac_sha256(key_m, c)
151
+ raise DecryptionError, 'HMAC verification failed' unless secure_compare(mac, expected_mac)
152
+
153
+ iv = c[0, 16]
154
+ ciphertext = c[16..]
155
+
156
+ begin
157
+ cipher = OpenSSL::Cipher.new('aes-256-cbc')
158
+ cipher.decrypt
159
+ cipher.key = key_e
160
+ cipher.iv = iv
161
+ cipher.update(ciphertext) + cipher.final
162
+ rescue OpenSSL::Cipher::CipherError => e
163
+ raise DecryptionError, "decryption failed: #{e.message}"
164
+ end
165
+ end
166
+
98
167
  class << self
99
168
  private
100
169
 
@@ -122,6 +191,19 @@ module BSV
122
191
 
123
192
  [iv, key_e, key_m]
124
193
  end
194
+
195
+ # Bitcore key derivation: SHA-512(ECDH X-coordinate) → key_e(32) + key_m(32)
196
+ def derive_bitcore_keys(private_key, public_key)
197
+ shared = private_key.derive_shared_secret(public_key)
198
+ # Bitcore uses raw X-coordinate (32 bytes BE), not compressed point
199
+ x_bytes = shared.point.to_octet_string(:uncompressed)[1, 32]
200
+ derived = Digest.sha512(x_bytes)
201
+
202
+ key_e = derived[0, 32]
203
+ key_m = derived[32, 32]
204
+
205
+ [key_e, key_m]
206
+ end
125
207
  end
126
208
  end
127
209
  end
@@ -32,12 +32,24 @@ module BSV
32
32
  # @return [Integer] cache TTL in seconds
33
33
  attr_reader :cache_ttl
34
34
 
35
+ DEFAULT_ARC_URL = 'https://arc.gorillapool.io'
36
+ DEFAULT_FALLBACK_RATE = 100
37
+
38
+ # Returns a LivePolicy with sensible defaults (GorillaPool ARC,
39
+ # 100 sat/kB fallback, 5-minute cache).
40
+ #
41
+ # @param api_key [String, nil] optional ARC API key
42
+ # @return [LivePolicy]
43
+ def self.default(api_key: nil)
44
+ new(arc_url: DEFAULT_ARC_URL, fallback_rate: DEFAULT_FALLBACK_RATE, api_key: api_key)
45
+ end
46
+
35
47
  # @param arc_url [String] ARC base URL (e.g. 'https://arc.gorillapool.io')
36
- # @param fallback_rate [Integer] sat/kB to use when fetch fails (default: 50)
48
+ # @param fallback_rate [Integer] sat/kB to use when fetch fails (default: 100)
37
49
  # @param cache_ttl [Integer] seconds to cache a fetched rate (default: 300)
38
50
  # @param api_key [String, nil] optional Bearer token for ARC authentication
39
51
  # @param http_client [#request, nil] injectable HTTP client for testing
40
- def initialize(arc_url:, fallback_rate: 50, cache_ttl: DEFAULT_CACHE_TTL, api_key: nil, http_client: nil)
52
+ def initialize(arc_url:, fallback_rate: DEFAULT_FALLBACK_RATE, cache_ttl: DEFAULT_CACHE_TTL, api_key: nil, http_client: nil)
41
53
  super()
42
54
  @arc_url = arc_url.chomp('/')
43
55
  @fallback_rate = fallback_rate
@@ -15,8 +15,8 @@ module BSV
15
15
  # @return [Integer] satoshis per kilobyte rate
16
16
  attr_reader :value
17
17
 
18
- # @param value [Integer] satoshis per kilobyte (default: 50)
19
- def initialize(value: 50)
18
+ # @param value [Integer] satoshis per kilobyte (default: 100)
19
+ def initialize(value: 100)
20
20
  super()
21
21
  @value = value
22
22
  end
data/lib/bsv/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BSV
4
- VERSION = '0.3.2'
4
+ VERSION = '0.4.0'
5
5
  end
@@ -25,7 +25,7 @@ module BSV
25
25
  @provider.fetch_utxos(address(network: network)).sum(&:satoshis)
26
26
  end
27
27
 
28
- def fund(tx, network: :mainnet, satoshis_per_byte: 0.5)
28
+ def fund(tx, network: :mainnet, satoshis_per_byte: 0.1)
29
29
  utxos = @provider.fetch_utxos(address(network: network))
30
30
  output_total = tx.total_output_satoshis
31
31
 
@@ -66,7 +66,7 @@ module BSV
66
66
  tx.sign_all(@private_key)
67
67
  end
68
68
 
69
- def fund_and_sign(tx, network: :mainnet, satoshis_per_byte: 0.5)
69
+ def fund_and_sign(tx, network: :mainnet, satoshis_per_byte: 0.1)
70
70
  fund(tx, network: network, satoshis_per_byte: satoshis_per_byte)
71
71
  sign(tx)
72
72
  end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'logger'
6
+
7
+ module BSV
8
+ module Wallet
9
+ # JSON file-backed storage adapter.
10
+ #
11
+ # Persists actions, outputs, and certificates as JSON files in a
12
+ # configurable directory (default: +~/.bsv-wallet/+). Data survives
13
+ # process restarts.
14
+ #
15
+ # Inherits all filtering and pagination logic from {MemoryStore} and
16
+ # adds load-on-init / save-on-mutation.
17
+ #
18
+ # @example Default location
19
+ # store = BSV::Wallet::FileStore.new
20
+ # # Data written to ~/.bsv-wallet/
21
+ #
22
+ # @example Custom directory
23
+ # store = BSV::Wallet::FileStore.new(dir: '/var/lib/my-app/wallet')
24
+ class FileStore < MemoryStore
25
+ DEFAULT_DIR = File.expand_path('~/.bsv-wallet')
26
+
27
+ # @param dir [String] directory for JSON files
28
+ # (default: +~/.bsv-wallet/+ or +BSV_WALLET_DIR+ env var)
29
+ # @param dir [String] directory for JSON files
30
+ # @param logger [Logger, nil] logger for permission warnings (default: Logger to STDERR)
31
+ def initialize(dir: nil, logger: nil)
32
+ super()
33
+ @dir = dir || ENV.fetch('BSV_WALLET_DIR', DEFAULT_DIR)
34
+ @logger = logger || Logger.new($stderr, progname: 'bsv-wallet')
35
+ FileUtils.mkdir_p(@dir, mode: 0o700)
36
+ check_permissions
37
+ load_from_disk
38
+ end
39
+
40
+ # @return [String] the storage directory path
41
+ attr_reader :dir
42
+
43
+ # --- Mutations: delegate to super, then persist ---
44
+
45
+ def store_action(action_data)
46
+ result = super
47
+ save_actions
48
+ result
49
+ end
50
+
51
+ def store_output(output_data)
52
+ result = super
53
+ save_outputs
54
+ result
55
+ end
56
+
57
+ def delete_output(outpoint)
58
+ result = super
59
+ save_outputs if result
60
+ result
61
+ end
62
+
63
+ def store_certificate(cert_data)
64
+ result = super
65
+ save_certificates
66
+ result
67
+ end
68
+
69
+ def delete_certificate(type:, serial_number:, certifier:)
70
+ result = super
71
+ save_certificates if result
72
+ result
73
+ end
74
+
75
+ private
76
+
77
+ def check_permissions
78
+ dir_mode = File.stat(@dir).mode & 0o777
79
+ if dir_mode != 0o700
80
+ @logger.warn("Wallet directory #{@dir} has permissions #{format('%04o', dir_mode)} (expected 0700). " \
81
+ 'Other users may be able to access wallet data.')
82
+ end
83
+
84
+ [actions_path, outputs_path, certificates_path].each do |path|
85
+ next unless File.exist?(path)
86
+
87
+ file_mode = File.stat(path).mode & 0o777
88
+ next if file_mode == 0o600
89
+
90
+ @logger.warn("Wallet file #{path} has permissions #{format('%04o', file_mode)} (expected 0600). " \
91
+ 'Other users may be able to read wallet data.')
92
+ end
93
+ end
94
+
95
+ def actions_path
96
+ File.join(@dir, 'actions.json')
97
+ end
98
+
99
+ def outputs_path
100
+ File.join(@dir, 'outputs.json')
101
+ end
102
+
103
+ def certificates_path
104
+ File.join(@dir, 'certificates.json')
105
+ end
106
+
107
+ def load_from_disk
108
+ @actions = load_file(actions_path)
109
+ @outputs = load_file(outputs_path)
110
+ @certificates = load_file(certificates_path)
111
+ end
112
+
113
+ def load_file(path)
114
+ return [] unless File.exist?(path)
115
+
116
+ data = JSON.parse(File.read(path))
117
+ return [] unless data.is_a?(Array)
118
+
119
+ # Symbolise top-level keys for consistency with MemoryStore
120
+ data.map { |entry| symbolise_keys(entry) }
121
+ rescue JSON::ParserError
122
+ []
123
+ end
124
+
125
+ def save_actions
126
+ write_file(actions_path, @actions)
127
+ end
128
+
129
+ def save_outputs
130
+ write_file(outputs_path, @outputs)
131
+ end
132
+
133
+ def save_certificates
134
+ write_file(certificates_path, @certificates)
135
+ end
136
+
137
+ def write_file(path, data)
138
+ json = JSON.pretty_generate(stringify_keys_deep(data))
139
+ tmp = "#{path}.tmp"
140
+ File.open(tmp, File::WRONLY | File::CREAT | File::TRUNC, 0o600) { |f| f.write(json) }
141
+ File.rename(tmp, path)
142
+ end
143
+
144
+ def symbolise_keys(hash)
145
+ return hash unless hash.is_a?(Hash)
146
+
147
+ hash.each_with_object({}) do |(k, v), result|
148
+ result[k.to_sym] = case v
149
+ when Hash then symbolise_keys(v)
150
+ when Array then v.map { |e| e.is_a?(Hash) ? symbolise_keys(e) : e }
151
+ else v
152
+ end
153
+ end
154
+ end
155
+
156
+ def stringify_keys_deep(obj)
157
+ case obj
158
+ when Hash
159
+ obj.each_with_object({}) do |(k, v), result|
160
+ result[k.to_s] = stringify_keys_deep(v)
161
+ end
162
+ when Array
163
+ obj.map { |e| stringify_keys_deep(e) }
164
+ else
165
+ obj
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BSV
4
4
  module WalletInterface
5
- VERSION = '0.1.2'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -36,11 +36,12 @@ module BSV
36
36
  attr_reader :network
37
37
 
38
38
  # @param key [BSV::Primitives::PrivateKey, String, KeyDeriver] signing key
39
- # @param storage [StorageAdapter] persistence adapter (default: MemoryStore)
39
+ # @param storage [StorageAdapter] persistence adapter (default: FileStore).
40
+ # Use +storage: MemoryStore.new+ for tests.
40
41
  # @param network [String] 'mainnet' (default) or 'testnet'
41
42
  # @param chain_provider [ChainProvider] blockchain data provider (default: NullChainProvider)
42
43
  # @param http_client [#request, nil] injectable HTTP client for certificate issuance
43
- def initialize(key, storage: MemoryStore.new, network: 'mainnet', chain_provider: NullChainProvider.new, http_client: nil)
44
+ def initialize(key, storage: FileStore.new, network: 'mainnet', chain_provider: NullChainProvider.new, http_client: nil)
44
45
  super(key)
45
46
  @storage = storage
46
47
  @network = network
@@ -13,6 +13,7 @@ module BSV
13
13
  autoload :Validators, 'bsv/wallet_interface/validators'
14
14
  autoload :StorageAdapter, 'bsv/wallet_interface/storage_adapter'
15
15
  autoload :MemoryStore, 'bsv/wallet_interface/memory_store'
16
+ autoload :FileStore, 'bsv/wallet_interface/file_store'
16
17
  autoload :ChainProvider, 'bsv/wallet_interface/chain_provider'
17
18
  autoload :NullChainProvider, 'bsv/wallet_interface/null_chain_provider'
18
19
  autoload :WalletClient, 'bsv/wallet_interface/wallet_client'
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bsv-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Bettison
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-03-30 00:00:00.000000000 Z
10
+ date: 2026-04-01 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: A Ruby library for interacting with the BSV Blockchain — keys, scripts,
13
13
  transactions, and more.
@@ -105,6 +105,7 @@ files:
105
105
  - lib/bsv/wallet_interface/errors/invalid_signature_error.rb
106
106
  - lib/bsv/wallet_interface/errors/unsupported_action_error.rb
107
107
  - lib/bsv/wallet_interface/errors/wallet_error.rb
108
+ - lib/bsv/wallet_interface/file_store.rb
108
109
  - lib/bsv/wallet_interface/interface.rb
109
110
  - lib/bsv/wallet_interface/key_deriver.rb
110
111
  - lib/bsv/wallet_interface/memory_store.rb