bsv-sdk 0.24.0 → 0.26.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 +4 -4
- data/CHANGELOG.md +62 -0
- data/README.md +4 -2
- data/lib/bsv/kv_store/entry.rb +15 -0
- data/lib/bsv/kv_store/global.rb +210 -0
- data/lib/bsv/kv_store/interpreter.rb +109 -0
- data/lib/bsv/kv_store/token.rb +10 -0
- data/lib/bsv/kv_store.rb +10 -0
- data/lib/bsv/mcp/tools/helpers.rb +3 -3
- data/lib/bsv/overlay/admin_token_template.rb +2 -2
- data/lib/bsv/overlay/historian.rb +118 -0
- data/lib/bsv/overlay/topic_broadcaster.rb +1 -1
- data/lib/bsv/overlay.rb +1 -0
- data/lib/bsv/primitives/digest.rb +107 -13
- data/lib/bsv/primitives/ecies.rb +12 -3
- data/lib/bsv/registry/client.rb +43 -1
- data/lib/bsv/script/bip276.rb +143 -0
- data/lib/bsv/script/interpreter/error.rb +2 -0
- data/lib/bsv/script/interpreter/interpreter.rb +226 -7
- data/lib/bsv/script/interpreter/operations/flow_control.rb +30 -13
- data/lib/bsv/script/push_drop_template.rb +2 -2
- data/lib/bsv/script/script.rb +21 -2
- data/lib/bsv/script.rb +1 -0
- data/lib/bsv/storage/downloader.rb +174 -0
- data/lib/bsv/storage/errors.rb +8 -0
- data/lib/bsv/storage/utils.rb +90 -0
- data/lib/bsv/storage.rb +16 -0
- data/lib/bsv/transaction/beef.rb +168 -14
- data/lib/bsv/transaction/beef_party.rb +119 -0
- data/lib/bsv/transaction/chain_tracker.rb +1 -1
- data/lib/bsv/transaction/fee_model.rb +1 -1
- data/lib/bsv/transaction/fee_models/live_policy.rb +1 -1
- data/lib/bsv/transaction/fee_models/satoshis_per_kilobyte.rb +1 -1
- data/lib/bsv/transaction/merkle_path.rb +1 -1
- data/lib/bsv/transaction/p2pkh.rb +1 -1
- data/lib/bsv/transaction/transaction_input.rb +78 -14
- data/lib/bsv/transaction/transaction_output.rb +46 -6
- data/lib/bsv/transaction/tx.rb +370 -39
- data/lib/bsv/transaction/unlocking_script_template.rb +2 -2
- data/lib/bsv/transaction.rb +1 -0
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv-sdk.rb +2 -0
- metadata +13 -2
- data/lib/bsv/secp256k1_native.bundle +0 -0
|
@@ -9,33 +9,111 @@ module BSV
|
|
|
9
9
|
# Thin wrappers around +OpenSSL::Digest+ and +OpenSSL::HMAC+ providing
|
|
10
10
|
# the hash algorithms used throughout the BSV protocol: SHA-1, SHA-256,
|
|
11
11
|
# double-SHA-256, SHA-512, RIPEMD-160, Hash160, HMAC, and PBKDF2.
|
|
12
|
+
#
|
|
13
|
+
# @note **Per-fibre cached OpenSSL contexts (via +Thread.current+)**
|
|
14
|
+
#
|
|
15
|
+
# +sha256+, +sha256d+, +sha1+, and +sha512+ each cache one
|
|
16
|
+
# +OpenSSL::Digest+ instance per fibre in +Thread.current+ under a
|
|
17
|
+
# namespaced key (e.g. +:bsv_sdk_sha256_digest+). Because MRI has no
|
|
18
|
+
# fibres in this SDK's +lib/+ today, "per fibre" and "per thread" are
|
|
19
|
+
# the same in practice — but the primitive is fibre-local, so the more
|
|
20
|
+
# precise term is used throughout. On every call the context is reset
|
|
21
|
+
# with +OpenSSL::Digest#reset+, which calls +EVP_DigestInit_ex+ against
|
|
22
|
+
# the +EVP_MD*+ already stored on the context. This skips the
|
|
23
|
+
# +EVP_get_digestbyname+ namemap lookup that
|
|
24
|
+
# +OpenSSL::Digest::SHA256.new+ (or +.digest+) performs on every fresh
|
|
25
|
+
# allocation. The namemap cost is paid exactly once per fibre per
|
|
26
|
+
# algorithm.
|
|
27
|
+
#
|
|
28
|
+
# **Fibre-local semantics.** +Thread.current[:key]+ is *fibre-local*
|
|
29
|
+
# in MRI (documented since Ruby 2.0, verified on 3.3 / 3.4 / 4.0) — each
|
|
30
|
+
# Fibre gets its own cached context. This is stronger than the SDK
|
|
31
|
+
# currently requires (no Fibres in +lib/+) and is correctly safe if
|
|
32
|
+
# Fibres are introduced later. The HLR body's "GVL makes it safe"
|
|
33
|
+
# rationale is subtly wrong; the correct reason is fibre-local scope.
|
|
34
|
+
#
|
|
35
|
+
# **Do not touch the cached instances externally.** The keys
|
|
36
|
+
# +:bsv_sdk_sha256_digest+, +:bsv_sdk_sha1_digest+, and
|
|
37
|
+
# +:bsv_sdk_sha512_digest+ are technically reachable via +Thread.current+
|
|
38
|
+
# but must never have +update+ called on them outside this module.
|
|
39
|
+
# Calling +update+ out-of-band would corrupt the next call's input.
|
|
40
|
+
#
|
|
41
|
+
# **User-visible invariants are unchanged:** return values are always
|
|
42
|
+
# +ASCII-8BIT+ binary strings; identical inputs always produce identical
|
|
43
|
+
# outputs; successive calls return distinct +String+ objects (output
|
|
44
|
+
# buffers are never cached).
|
|
45
|
+
#
|
|
46
|
+
# **Prefix convention.** +:bsv_sdk_<algo>_digest+ is the SDK-wide
|
|
47
|
+
# naming convention for any future +Thread.current+ usage in this
|
|
48
|
+
# codebase — the prefix avoids collision with other libraries that may
|
|
49
|
+
# also use +Thread.current+.
|
|
50
|
+
#
|
|
51
|
+
# **FIPS.** SHA-1, SHA-256, and SHA-512 remain FIPS-approved algorithms
|
|
52
|
+
# under this pattern; the algorithm fetch happens once at +.new+, not per
|
|
53
|
+
# call.
|
|
54
|
+
#
|
|
55
|
+
# **CI matrix.** Verified on MRI 3.3, 3.4, and 4.0. JRuby and
|
|
56
|
+
# TruffleRuby are not in the CI matrix — per-thread caching is correct on
|
|
57
|
+
# true-parallel Rubies (each thread has independent +Thread.current+) but
|
|
58
|
+
# unverified here.
|
|
59
|
+
#
|
|
60
|
+
# **Why other SDKs do not do this.** The TypeScript, Go, and Python SDKs
|
|
61
|
+
# allocate fresh digest contexts per call because their OpenSSL bindings
|
|
62
|
+
# cache the algorithm handle internally. MRI's +openssl+ gem bridges
|
|
63
|
+
# through +EVP_get_digestbyname+ on every allocation — we are patching
|
|
64
|
+
# the Ruby binding overhead, not diverging from BSV protocol semantics.
|
|
65
|
+
#
|
|
66
|
+
# **Out of scope with rationale:**
|
|
67
|
+
# - +ripemd160+ — pure-Ruby implementation; no OpenSSL context.
|
|
68
|
+
# - +hmac_sha256+ / +hmac_sha512+ — HMAC key changes per call. A future
|
|
69
|
+
# cache *must* key on wrapper-object identity (+object_id+), never on
|
|
70
|
+
# key bytes — a key-bytes cache lookup would create a secret-dependent
|
|
71
|
+
# side-channel via cache-hit timing.
|
|
72
|
+
# - +pbkdf2_hmac_sha512+ — one-shot at BIP-39 seed derivation; not a
|
|
73
|
+
# hot path.
|
|
12
74
|
module Digest
|
|
13
75
|
module_function
|
|
14
76
|
|
|
15
77
|
# Compute SHA-1 digest.
|
|
16
78
|
#
|
|
17
79
|
# @param data [String] binary data to hash
|
|
18
|
-
# @return [String] 20-byte digest
|
|
80
|
+
# @return [String] 20-byte digest (ASCII-8BIT)
|
|
19
81
|
def sha1(data)
|
|
20
|
-
OpenSSL::Digest
|
|
82
|
+
d = Thread.current[:bsv_sdk_sha1_digest] ||= OpenSSL::Digest.new('SHA1')
|
|
83
|
+
d.reset
|
|
84
|
+
d.update(data)
|
|
85
|
+
d.digest
|
|
21
86
|
end
|
|
22
87
|
|
|
23
88
|
# Compute SHA-256 digest.
|
|
24
89
|
#
|
|
25
90
|
# @param data [String] binary data to hash
|
|
26
|
-
# @return [String] 32-byte digest
|
|
91
|
+
# @return [String] 32-byte digest (ASCII-8BIT)
|
|
27
92
|
def sha256(data)
|
|
28
|
-
OpenSSL::Digest
|
|
93
|
+
d = Thread.current[:bsv_sdk_sha256_digest] ||= OpenSSL::Digest.new('SHA256')
|
|
94
|
+
d.reset
|
|
95
|
+
d.update(data)
|
|
96
|
+
d.digest
|
|
29
97
|
end
|
|
30
98
|
|
|
31
99
|
# Compute double-SHA-256 (SHA-256d) digest.
|
|
32
100
|
#
|
|
33
101
|
# Used extensively in Bitcoin for transaction and block hashing.
|
|
102
|
+
# Inlines the two-round chain against the same cached SHA-256 context,
|
|
103
|
+
# saving one context allocation, one namemap lookup, one intermediate
|
|
104
|
+
# +String+, and one +module_function+ dispatch per call compared with
|
|
105
|
+
# the naive +sha256(sha256(data))+ decomposition.
|
|
34
106
|
#
|
|
35
107
|
# @param data [String] binary data to hash
|
|
36
|
-
# @return [String] 32-byte digest
|
|
108
|
+
# @return [String] 32-byte digest (ASCII-8BIT)
|
|
37
109
|
def sha256d(data)
|
|
38
|
-
|
|
110
|
+
d = Thread.current[:bsv_sdk_sha256_digest] ||= OpenSSL::Digest.new('SHA256')
|
|
111
|
+
d.reset
|
|
112
|
+
d.update(data)
|
|
113
|
+
first = d.digest
|
|
114
|
+
d.reset # defensive: some Ruby versions internally reset after #digest
|
|
115
|
+
d.update(first)
|
|
116
|
+
d.digest
|
|
39
117
|
end
|
|
40
118
|
|
|
41
119
|
alias hash256 sha256d
|
|
@@ -44,15 +122,23 @@ module BSV
|
|
|
44
122
|
# Compute SHA-512 digest.
|
|
45
123
|
#
|
|
46
124
|
# @param data [String] binary data to hash
|
|
47
|
-
# @return [String] 64-byte digest
|
|
125
|
+
# @return [String] 64-byte digest (ASCII-8BIT)
|
|
48
126
|
def sha512(data)
|
|
49
|
-
OpenSSL::Digest
|
|
127
|
+
d = Thread.current[:bsv_sdk_sha512_digest] ||= OpenSSL::Digest.new('SHA512')
|
|
128
|
+
d.reset
|
|
129
|
+
d.update(data)
|
|
130
|
+
d.digest
|
|
50
131
|
end
|
|
51
132
|
|
|
52
133
|
# Compute RIPEMD-160 digest.
|
|
53
134
|
#
|
|
135
|
+
# @note Uses the pure-Ruby {BSV::Primitives::Ripemd160} implementation —
|
|
136
|
+
# no OpenSSL context is held or cached here. RIPEMD-160 is not in
|
|
137
|
+
# scope for the per-thread caching optimisation (different optimisation
|
|
138
|
+
# track; not currently a hot path).
|
|
139
|
+
#
|
|
54
140
|
# @param data [String] binary data to hash
|
|
55
|
-
# @return [String] 20-byte digest
|
|
141
|
+
# @return [String] 20-byte digest (ASCII-8BIT)
|
|
56
142
|
def ripemd160(data)
|
|
57
143
|
Ripemd160.digest(data)
|
|
58
144
|
end
|
|
@@ -62,25 +148,32 @@ module BSV
|
|
|
62
148
|
# Standard Bitcoin hash used for addresses and P2PKH script matching.
|
|
63
149
|
#
|
|
64
150
|
# @param data [String] binary data to hash
|
|
65
|
-
# @return [String] 20-byte digest
|
|
151
|
+
# @return [String] 20-byte digest (ASCII-8BIT)
|
|
66
152
|
def hash160(data)
|
|
67
153
|
ripemd160(sha256(data))
|
|
68
154
|
end
|
|
69
155
|
|
|
70
156
|
# Compute HMAC-SHA-256.
|
|
71
157
|
#
|
|
158
|
+
# @note HMAC context reuse is deferred — the key changes per call, so a
|
|
159
|
+
# future cache must key on wrapper-object identity (+object_id+), never
|
|
160
|
+
# on key bytes, to avoid a secret-dependent side-channel via cache-hit
|
|
161
|
+
# timing.
|
|
162
|
+
#
|
|
72
163
|
# @param key [String] HMAC key
|
|
73
164
|
# @param data [String] data to authenticate
|
|
74
|
-
# @return [String] 32-byte MAC
|
|
165
|
+
# @return [String] 32-byte MAC (ASCII-8BIT)
|
|
75
166
|
def hmac_sha256(key, data)
|
|
76
167
|
OpenSSL::HMAC.digest('SHA256', key, data)
|
|
77
168
|
end
|
|
78
169
|
|
|
79
170
|
# Compute HMAC-SHA-512.
|
|
80
171
|
#
|
|
172
|
+
# @note HMAC context reuse is deferred — see +hmac_sha256+ note.
|
|
173
|
+
#
|
|
81
174
|
# @param key [String] HMAC key
|
|
82
175
|
# @param data [String] data to authenticate
|
|
83
|
-
# @return [String] 64-byte MAC
|
|
176
|
+
# @return [String] 64-byte MAC (ASCII-8BIT)
|
|
84
177
|
def hmac_sha512(key, data)
|
|
85
178
|
OpenSSL::HMAC.digest('SHA512', key, data)
|
|
86
179
|
end
|
|
@@ -88,12 +181,13 @@ module BSV
|
|
|
88
181
|
# Derive a key using PBKDF2-HMAC-SHA-512.
|
|
89
182
|
#
|
|
90
183
|
# Used by BIP-39 to convert mnemonic phrases into seeds.
|
|
184
|
+
# One-shot at key creation — not a hot path; no context reuse applied.
|
|
91
185
|
#
|
|
92
186
|
# @param password [String] the password (mnemonic phrase)
|
|
93
187
|
# @param salt [String] the salt (+"mnemonic"+ + passphrase)
|
|
94
188
|
# @param iterations [Integer] iteration count (default: 2048 per BIP-39)
|
|
95
189
|
# @param key_length [Integer] desired output length in bytes (default: 64)
|
|
96
|
-
# @return [String] derived key bytes
|
|
190
|
+
# @return [String] derived key bytes (ASCII-8BIT)
|
|
97
191
|
def pbkdf2_hmac_sha512(password, salt, iterations: 2048, key_length: 64)
|
|
98
192
|
OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, 'sha512')
|
|
99
193
|
end
|
data/lib/bsv/primitives/ecies.rb
CHANGED
|
@@ -147,15 +147,24 @@ module BSV
|
|
|
147
147
|
# @param message [String] the plaintext message
|
|
148
148
|
# @param public_key [PublicKey] the recipient's public key
|
|
149
149
|
# @param private_key [PrivateKey, nil] optional ephemeral key (random if omitted)
|
|
150
|
+
# @param iv [String, nil] optional 16-byte ASCII-8BIT IV. When omitted a random IV is
|
|
151
|
+
# generated via +SecureRandom+. Supply a fixed value only for deterministic test
|
|
152
|
+
# vectors — **never use a fixed IV in production**.
|
|
150
153
|
# @return [String] encrypted payload
|
|
151
|
-
|
|
154
|
+
# @raise [ArgumentError] if +iv+ is supplied but is not exactly 16 bytes
|
|
155
|
+
def bitcore_encrypt(message, public_key, private_key: nil, iv: nil)
|
|
152
156
|
message = message.b if message.encoding != Encoding::ASCII_8BIT
|
|
153
157
|
|
|
158
|
+
if iv
|
|
159
|
+
iv = iv.b if iv.encoding != Encoding::ASCII_8BIT
|
|
160
|
+
raise ArgumentError, 'iv must be exactly 16 bytes' unless iv.bytesize == 16
|
|
161
|
+
else
|
|
162
|
+
iv = SecureRandom.random_bytes(16)
|
|
163
|
+
end
|
|
164
|
+
|
|
154
165
|
ephemeral = private_key || PrivateKey.generate
|
|
155
166
|
key_e, key_m = derive_bitcore_keys(ephemeral, public_key)
|
|
156
167
|
|
|
157
|
-
iv = SecureRandom.random_bytes(16)
|
|
158
|
-
|
|
159
168
|
cipher = OpenSSL::Cipher.new('aes-256-cbc')
|
|
160
169
|
cipher.encrypt
|
|
161
170
|
cipher.key = key_e
|
data/lib/bsv/registry/client.rb
CHANGED
|
@@ -112,6 +112,48 @@ module BSV
|
|
|
112
112
|
end
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
# Resolves basket registry definitions.
|
|
116
|
+
#
|
|
117
|
+
# Thin wrapper around {#resolve} for cross-SDK parity with the Go SDK's
|
|
118
|
+
# +ResolveBasket+ method.
|
|
119
|
+
#
|
|
120
|
+
# @param query [Hash] optional filter criteria:
|
|
121
|
+
# - +:basket_id+ [String] exact basket identifier
|
|
122
|
+
# - +:name+ [String] human-readable basket name
|
|
123
|
+
# - +:registry_operators+ [Array<String>] operator public key hexes
|
|
124
|
+
# @return [Array<RegisteredDefinition>] matching registered basket definitions
|
|
125
|
+
def resolve_basket(query = {})
|
|
126
|
+
resolve(DefinitionType::BASKET, query)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Resolves protocol registry definitions.
|
|
130
|
+
#
|
|
131
|
+
# Thin wrapper around {#resolve} for cross-SDK parity with the Go SDK's
|
|
132
|
+
# +ResolveProtocol+ method.
|
|
133
|
+
#
|
|
134
|
+
# @param query [Hash] optional filter criteria:
|
|
135
|
+
# - +:name+ [String] human-readable protocol name
|
|
136
|
+
# - +:protocol_id+ [Array] BRC-43 two-element protocol ID, e.g. +[1, 'protomap']+
|
|
137
|
+
# - +:registry_operators+ [Array<String>] operator public key hexes
|
|
138
|
+
# @return [Array<RegisteredDefinition>] matching registered protocol definitions
|
|
139
|
+
def resolve_protocol(query = {})
|
|
140
|
+
resolve(DefinitionType::PROTOCOL, query)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Resolves certificate type registry definitions.
|
|
144
|
+
#
|
|
145
|
+
# Thin wrapper around {#resolve} for cross-SDK parity with the Go SDK's
|
|
146
|
+
# +ResolveCertificate+ method.
|
|
147
|
+
#
|
|
148
|
+
# @param query [Hash] optional filter criteria:
|
|
149
|
+
# - +:type+ [String] Base64-encoded certificate type identifier
|
|
150
|
+
# - +:name+ [String] human-readable certificate type name
|
|
151
|
+
# - +:registry_operators+ [Array<String>] operator public key hexes
|
|
152
|
+
# @return [Array<RegisteredDefinition>] matching registered certificate type definitions
|
|
153
|
+
def resolve_certificate(query = {})
|
|
154
|
+
resolve(DefinitionType::CERTIFICATE, query)
|
|
155
|
+
end
|
|
156
|
+
|
|
115
157
|
# Lists the registry operator's own published definitions for the given type.
|
|
116
158
|
#
|
|
117
159
|
# Queries the wallet for spendable outputs in the appropriate basket,
|
|
@@ -430,7 +472,7 @@ module BSV
|
|
|
430
472
|
#
|
|
431
473
|
# @param definition_type [String]
|
|
432
474
|
# @param output [Hash] wallet output with :outpoint, :satoshis keys
|
|
433
|
-
# @param beef [
|
|
475
|
+
# @param beef [Transaction::Beef] parsed BEEF
|
|
434
476
|
# @param beef_raw [String] raw BEEF bytes
|
|
435
477
|
# @return [RegisteredDefinition, nil]
|
|
436
478
|
def parse_own_output_to_registered_definition(definition_type, output, beef, beef_raw)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bsv/primitives/digest'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module Script
|
|
7
|
+
# BIP-276 text encoding for scripts and templates.
|
|
8
|
+
#
|
|
9
|
+
# Encodes and decodes typed bitcoin data using the scheme described in
|
|
10
|
+
# https://github.com/moneybutton/bips/blob/master/bip-0276.mediawiki
|
|
11
|
+
#
|
|
12
|
+
# Format: `<prefix>:<version-byte><network-byte><hex-payload><checksum>`
|
|
13
|
+
# where version and network are each one byte (two hex digits), and
|
|
14
|
+
# checksum is the first 4 bytes of double-SHA256 over the full preimage
|
|
15
|
+
# (the string up to and including the payload), hex-encoded.
|
|
16
|
+
#
|
|
17
|
+
# @note Field order is version-then-network, matching the BIP-276 spec
|
|
18
|
+
# and the Go SDK reference. The Python SDK has these reversed — a known
|
|
19
|
+
# py-sdk bug that is invisible at default v=1, n=1.
|
|
20
|
+
module BIP276
|
|
21
|
+
# Returned by {decode}; immutable value object.
|
|
22
|
+
Result = Data.define(:prefix, :version, :network, :data)
|
|
23
|
+
|
|
24
|
+
# Custom exception hierarchy.
|
|
25
|
+
class Error < StandardError; end
|
|
26
|
+
class InvalidFormat < Error; end
|
|
27
|
+
class InvalidChecksum < Error; end
|
|
28
|
+
|
|
29
|
+
PREFIX_SCRIPT = 'bitcoin-script'
|
|
30
|
+
PREFIX_TEMPLATE = 'bitcoin-template'
|
|
31
|
+
CURRENT_VERSION = 1
|
|
32
|
+
NETWORK_MAINNET = 1
|
|
33
|
+
NETWORK_TESTNET = 2
|
|
34
|
+
|
|
35
|
+
# Regex: prefix(:)(version 2 hex)(network 2 hex)(data hex*)(checksum 8 hex)
|
|
36
|
+
VALID_BIP276 = /\A(.+?):([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]*)([0-9A-Fa-f]{8})\z/
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# Encode a binary payload as a BIP-276 string.
|
|
41
|
+
#
|
|
42
|
+
# @param data [String] binary payload (e.g. a {Script::Script}'s binary form)
|
|
43
|
+
# @param prefix [String] {PREFIX_SCRIPT} or {PREFIX_TEMPLATE} (or any custom prefix)
|
|
44
|
+
# @param version [Integer] 1..255 — {CURRENT_VERSION} by default
|
|
45
|
+
# @param network [Integer] 1..255 — {NETWORK_MAINNET} by default
|
|
46
|
+
# @return [String] BIP-276 encoded string
|
|
47
|
+
# @raise [ArgumentError] if version or network is out of the range 1..255
|
|
48
|
+
def encode(data, prefix: PREFIX_SCRIPT, network: NETWORK_MAINNET, version: CURRENT_VERSION)
|
|
49
|
+
raise ArgumentError, "version must be 1..255, got #{version}" unless (1..255).cover?(version)
|
|
50
|
+
raise ArgumentError, "network must be 1..255, got #{network}" unless (1..255).cover?(network)
|
|
51
|
+
|
|
52
|
+
preimage = build_preimage(prefix, version, network, data)
|
|
53
|
+
preimage + checksum(preimage)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Decode a BIP-276 string.
|
|
57
|
+
#
|
|
58
|
+
# @param str [String] BIP-276 encoded string
|
|
59
|
+
# @return [Result] value object with +:prefix+, +:version+, +:network+, +:data+
|
|
60
|
+
# @raise [InvalidFormat] if the string is structurally malformed
|
|
61
|
+
# @raise [InvalidChecksum] if the checksum does not match the payload
|
|
62
|
+
def decode(str)
|
|
63
|
+
match = VALID_BIP276.match(str)
|
|
64
|
+
raise InvalidFormat, 'not a valid BIP-276 string' unless match
|
|
65
|
+
|
|
66
|
+
prefix = match[1]
|
|
67
|
+
version = match[2].to_i(16)
|
|
68
|
+
network = match[3].to_i(16)
|
|
69
|
+
|
|
70
|
+
raise InvalidFormat, 'data payload has odd number of hex digits' if match[4].length.odd?
|
|
71
|
+
|
|
72
|
+
data = [match[4]].pack('H*')
|
|
73
|
+
|
|
74
|
+
# Compute the checksum over the EXACT input bytes (everything except
|
|
75
|
+
# the trailing 8-hex-digit checksum) rather than rebuilding the
|
|
76
|
+
# preimage from parsed fields. Rebuilding via build_preimage would
|
|
77
|
+
# lowercase the payload hex, causing valid mixed-case input to fail.
|
|
78
|
+
preimage = str[0..-9]
|
|
79
|
+
expected = checksum(preimage)
|
|
80
|
+
raise InvalidChecksum, 'checksum mismatch' unless match[5].downcase == expected
|
|
81
|
+
|
|
82
|
+
Result.new(prefix: prefix, version: version, network: network, data: data)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Encode a script payload using the {PREFIX_SCRIPT} prefix.
|
|
86
|
+
#
|
|
87
|
+
# @param (see encode)
|
|
88
|
+
# @return [String] BIP-276 encoded string with +bitcoin-script+ prefix
|
|
89
|
+
def encode_script(data, network: NETWORK_MAINNET, version: CURRENT_VERSION)
|
|
90
|
+
encode(data, prefix: PREFIX_SCRIPT, network: network, version: version)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Encode a template payload using the {PREFIX_TEMPLATE} prefix.
|
|
94
|
+
#
|
|
95
|
+
# @param (see encode)
|
|
96
|
+
# @return [String] BIP-276 encoded string with +bitcoin-template+ prefix
|
|
97
|
+
def encode_template(data, network: NETWORK_MAINNET, version: CURRENT_VERSION)
|
|
98
|
+
encode(data, prefix: PREFIX_TEMPLATE, network: network, version: version)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Decode a BIP-276 string that must have the {PREFIX_SCRIPT} prefix.
|
|
102
|
+
#
|
|
103
|
+
# @param str [String] BIP-276 encoded string
|
|
104
|
+
# @return [Result]
|
|
105
|
+
# @raise [InvalidFormat] if prefix is not +bitcoin-script+
|
|
106
|
+
# @raise [InvalidChecksum] if the checksum does not match
|
|
107
|
+
def decode_script(str)
|
|
108
|
+
result = decode(str)
|
|
109
|
+
raise InvalidFormat, "expected prefix '#{PREFIX_SCRIPT}', got '#{result.prefix}'" \
|
|
110
|
+
unless result.prefix == PREFIX_SCRIPT
|
|
111
|
+
|
|
112
|
+
result
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Decode a BIP-276 string that must have the {PREFIX_TEMPLATE} prefix.
|
|
116
|
+
#
|
|
117
|
+
# @param str [String] BIP-276 encoded string
|
|
118
|
+
# @return [Result]
|
|
119
|
+
# @raise [InvalidFormat] if prefix is not +bitcoin-template+
|
|
120
|
+
# @raise [InvalidChecksum] if the checksum does not match
|
|
121
|
+
def decode_template(str)
|
|
122
|
+
result = decode(str)
|
|
123
|
+
raise InvalidFormat, "expected prefix '#{PREFIX_TEMPLATE}', got '#{result.prefix}'" \
|
|
124
|
+
unless result.prefix == PREFIX_TEMPLATE
|
|
125
|
+
|
|
126
|
+
result
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @api private
|
|
130
|
+
def build_preimage(prefix, version, network, data)
|
|
131
|
+
format('%<prefix>s:%<version>02x%<network>02x%<payload>s',
|
|
132
|
+
prefix: prefix, version: version, network: network, payload: data.unpack1('H*'))
|
|
133
|
+
end
|
|
134
|
+
private_class_method :build_preimage
|
|
135
|
+
|
|
136
|
+
# @api private
|
|
137
|
+
def checksum(preimage)
|
|
138
|
+
BSV::Primitives::Digest.sha256d(preimage)[0, 4].unpack1('H*')
|
|
139
|
+
end
|
|
140
|
+
private_class_method :checksum
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|