bsv-sdk 0.24.0 → 0.25.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 +28 -0
- 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/ecies.rb +12 -3
- data/lib/bsv/registry/client.rb +43 -1
- data/lib/bsv/script/bip276.rb +143 -0
- data/lib/bsv/script/push_drop_template.rb +2 -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 +1 -1
- data/lib/bsv/transaction/tx.rb +11 -11
- 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 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a0dbc7f524004e1db715058dd17d336e62f99687239ec4c7807512d730e6ef5
|
|
4
|
+
data.tar.gz: 45b1fc354cbfe718a38e01c0d4896d537600d188bba909f351da1bc17bac66c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e212bf3c077ab32a6d5187c0e3cbd93fd61f26844d40dce31b28c0574a1111db3a081dd0d843be90dc682779ede74ece03f0f7eccc0eec930a372306cdc3ca2
|
|
7
|
+
data.tar.gz: ec1e1b2e0c7b5ae142b52cb1863000fd262d0c1dc30346c2a7d412af2e1f66cbc3057c864bc9d6229737be59c30eae2eaf6d7fee7c296a9393c6e0f8b636b558
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ All notable changes to the `bsv-sdk` gem are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
6
6
|
and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## 0.25.0 — 2026-06-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `BSV::Storage::Utils` — UHRP URL helpers (`normalise_url`, `url_for_hash`,
|
|
12
|
+
`url_for_file`, `hash_from_url`, `valid_url?`) (#817).
|
|
13
|
+
- `BSV::Overlay::Historian` — generic output-history walker over `LookupResolver`,
|
|
14
|
+
enabling protocol-specific replay of an outpoint's lineage (#818).
|
|
15
|
+
- `BSV::Registry` — typed `resolve_basket`, `resolve_protocol`, and
|
|
16
|
+
`resolve_certificate` methods on the registry client (#819).
|
|
17
|
+
- `BSV::KVStore::Interpreter` — `Historian`-compatible interpreter for KV
|
|
18
|
+
protocol outputs (#820).
|
|
19
|
+
- `BSV::Storage::Downloader` — UHRP resolver and content fetcher with
|
|
20
|
+
hash verification (#821).
|
|
21
|
+
- `BSV::KVStore::Global` — overlay-backed read-only key-value reader (#822).
|
|
22
|
+
- `BSV::Script::BIP276` — text encoding for scripts and templates (`encode`,
|
|
23
|
+
`decode`) (#830).
|
|
24
|
+
- `BSV::Transaction::BeefParty` — `Beef` subclass tracking per-party knowledge
|
|
25
|
+
of transactions for multi-party BEEF exchange, plus supporting methods on
|
|
26
|
+
`Beef` (#831).
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- `Beef` wire-inputs debug log now uses the existing `TransactionInput#dtxid_hex`
|
|
30
|
+
and `Tx#dtxid` accessors instead of inlined byte-order conversions (#828).
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- ECIES bitcore variant — added `iv:` kwarg and TypeScript SDK conformance
|
|
34
|
+
vector (#829).
|
|
35
|
+
|
|
8
36
|
## 0.24.0 — 2026-06-11
|
|
9
37
|
|
|
10
38
|
### Breaking Changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module KVStore
|
|
5
|
+
# An immutable KVStore entry returned from {Global#get}.
|
|
6
|
+
#
|
|
7
|
+
# +token+ is populated only when +include_token: true+ is passed to +#get+.
|
|
8
|
+
# +history+ is populated only when +history: true+ is passed to +#get+.
|
|
9
|
+
Entry = Data.define(:key, :value, :controller, :protocol_id, :tags, :token, :history) do
|
|
10
|
+
def initialize(key:, value:, controller:, protocol_id:, tags:, token: nil, history: nil)
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module KVStore
|
|
7
|
+
# Read-only client for the global KVStore overlay service.
|
|
8
|
+
#
|
|
9
|
+
# Queries the +ls_kvstore+ lookup service via a {BSV::Overlay::LookupResolver},
|
|
10
|
+
# decodes PushDrop tokens (5-field legacy and 6-field current formats), verifies
|
|
11
|
+
# the token signature, and returns typed {Entry} objects.
|
|
12
|
+
#
|
|
13
|
+
# Set/remove operations require wallet writes and are out of scope — those live
|
|
14
|
+
# in the +bsv-wallet+ gem.
|
|
15
|
+
#
|
|
16
|
+
# == Selector requirement
|
|
17
|
+
#
|
|
18
|
+
# {#get} raises +ArgumentError+ unless at least one of +key+, +controller+,
|
|
19
|
+
# +protocol_id+, or a non-empty +tags+ array is supplied in the query.
|
|
20
|
+
#
|
|
21
|
+
# == Always returns Array
|
|
22
|
+
#
|
|
23
|
+
# {#get} always returns +Array<Entry>+, even for single-result selectors such
|
|
24
|
+
# as +key + controller+. Callers that expect at most one result should use
|
|
25
|
+
# +.first+.
|
|
26
|
+
#
|
|
27
|
+
# == Silent skipping
|
|
28
|
+
#
|
|
29
|
+
# Outputs that fail BEEF parsing, PushDrop decoding, field-count validation, or
|
|
30
|
+
# signature verification are silently skipped (matching the TS SDK contract).
|
|
31
|
+
class Global
|
|
32
|
+
# @param network_preset [Symbol] :mainnet, :testnet, or :local
|
|
33
|
+
# @param lookup_resolver [BSV::Overlay::LookupResolver, nil] injectable resolver
|
|
34
|
+
# @param service_name [String] lookup service identifier
|
|
35
|
+
# @param proto_wallet [BSV::Wallet::ProtoWallet, nil] injectable wallet used for
|
|
36
|
+
# signature verification; defaults to +ProtoWallet.new('anyone')+. Tests can
|
|
37
|
+
# substitute a double to avoid +allow_any_instance_of+.
|
|
38
|
+
def initialize(network_preset: :mainnet, lookup_resolver: nil, service_name: 'ls_kvstore', proto_wallet: nil)
|
|
39
|
+
@lookup_resolver = lookup_resolver || BSV::Overlay::LookupResolver.new(network_preset: network_preset)
|
|
40
|
+
@service_name = service_name
|
|
41
|
+
@proto_wallet = proto_wallet || BSV::Wallet::ProtoWallet.new('anyone')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Query the overlay for KVStore entries.
|
|
45
|
+
#
|
|
46
|
+
# @param query [Hash] selector: +:key+, +:controller+, +:protocol_id+, +:tags+,
|
|
47
|
+
# +:tag_query_mode+. At least one of the first four must be present.
|
|
48
|
+
# @param include_token [Boolean] populate {Token} on each entry when true
|
|
49
|
+
# @param history [Boolean] populate history via {BSV::Overlay::Historian} when true
|
|
50
|
+
# @return [Array<Entry>] matching entries (empty array when nothing matches)
|
|
51
|
+
# @raise [ArgumentError] if no selector is provided
|
|
52
|
+
def get(query, include_token: false, history: false)
|
|
53
|
+
validate_selector!(query)
|
|
54
|
+
|
|
55
|
+
question = BSV::Overlay::LookupQuestion.new(
|
|
56
|
+
service: @service_name,
|
|
57
|
+
query: camelise(query)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
answer = @lookup_resolver.query(question)
|
|
61
|
+
return [] unless answer.type == 'output-list'
|
|
62
|
+
|
|
63
|
+
answer.outputs.filter_map do |output|
|
|
64
|
+
build_entry(output, include_token: include_token, history: history)
|
|
65
|
+
rescue StandardError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# ---- Selector validation ----
|
|
73
|
+
|
|
74
|
+
def validate_selector!(query)
|
|
75
|
+
raise ArgumentError, 'query must be a Hash' unless query.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
has_key = query[:key].is_a?(String) && !query[:key].empty?
|
|
78
|
+
has_controller = query[:controller].is_a?(String) && !query[:controller].empty?
|
|
79
|
+
has_protocol = query[:protocol_id].is_a?(Array) && query[:protocol_id].length == 2
|
|
80
|
+
has_tags = query[:tags].is_a?(Array) && !query[:tags].empty?
|
|
81
|
+
|
|
82
|
+
return if has_key || has_controller || has_protocol || has_tags
|
|
83
|
+
|
|
84
|
+
raise ArgumentError,
|
|
85
|
+
'At least one selector required (key, controller, protocol_id, or non-empty tags)'
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# ---- Entry construction ----
|
|
89
|
+
|
|
90
|
+
def build_entry(output, include_token:, history:)
|
|
91
|
+
beef_data = output['beef'] || output[:beef]
|
|
92
|
+
output_index = (output['outputIndex'] || output[:output_index] || 0).to_i
|
|
93
|
+
raise 'Negative outputIndex' if output_index.negative?
|
|
94
|
+
|
|
95
|
+
beef = parse_beef!(beef_data)
|
|
96
|
+
beef_tx = beef.transactions.last
|
|
97
|
+
raise 'No transaction in BEEF' if beef_tx.nil? || beef_tx.is_a?(BSV::Transaction::Beef::TxidOnlyEntry)
|
|
98
|
+
|
|
99
|
+
tx = beef_tx.transaction
|
|
100
|
+
txout = tx.outputs[output_index]
|
|
101
|
+
raise 'outputIndex out of range' if txout.nil?
|
|
102
|
+
|
|
103
|
+
locking_script = txout.locking_script
|
|
104
|
+
raise 'No locking script' unless locking_script&.pushdrop?
|
|
105
|
+
|
|
106
|
+
raw_fields = locking_script.pushdrop_fields
|
|
107
|
+
raise 'Not a PushDrop script' unless raw_fields
|
|
108
|
+
|
|
109
|
+
field_count = raw_fields.length
|
|
110
|
+
raise "Unexpected field count: #{field_count}" unless [5, 6].include?(field_count)
|
|
111
|
+
|
|
112
|
+
# Separate data fields from trailing signature field
|
|
113
|
+
data_fields = raw_fields[0...-1]
|
|
114
|
+
sig_bytes = raw_fields.last
|
|
115
|
+
|
|
116
|
+
# Decode common fields (indices match kvProtocol in TS SDK)
|
|
117
|
+
protocol_id = decode_protocol_id!(data_fields[0])
|
|
118
|
+
key = decode_utf8!(data_fields[1])
|
|
119
|
+
value = decode_utf8!(data_fields[2])
|
|
120
|
+
controller = data_fields[3].unpack1('H*')
|
|
121
|
+
|
|
122
|
+
# New format (6 fields) has tags at index 4; old format (5 fields) has no tags field
|
|
123
|
+
tags = if field_count == 6
|
|
124
|
+
begin
|
|
125
|
+
JSON.parse(decode_utf8!(data_fields[4]))
|
|
126
|
+
rescue StandardError
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
verify_signature!(data_fields, sig_bytes, protocol_id, key, controller)
|
|
132
|
+
|
|
133
|
+
token = if include_token
|
|
134
|
+
Token.new(
|
|
135
|
+
dtxid: tx.dtxid_hex,
|
|
136
|
+
output_index: output_index,
|
|
137
|
+
beef: beef,
|
|
138
|
+
satoshis: txout.satoshis || 0
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
entry_history = if history
|
|
143
|
+
BSV::Overlay::Historian
|
|
144
|
+
.new(BSV::KVStore::Interpreter)
|
|
145
|
+
.build_history(tx, { key: key, protocol_id: protocol_id })
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
Entry.new(
|
|
149
|
+
key: key,
|
|
150
|
+
value: value,
|
|
151
|
+
controller: controller,
|
|
152
|
+
protocol_id: protocol_id,
|
|
153
|
+
tags: tags,
|
|
154
|
+
token: token,
|
|
155
|
+
history: entry_history
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# ---- Field decoding helpers ----
|
|
160
|
+
|
|
161
|
+
def parse_beef!(beef_data)
|
|
162
|
+
case beef_data
|
|
163
|
+
when String
|
|
164
|
+
BSV::Transaction::Beef.from_binary(beef_data)
|
|
165
|
+
when Array
|
|
166
|
+
BSV::Transaction::Beef.from_binary(beef_data.pack('C*'))
|
|
167
|
+
else
|
|
168
|
+
raise 'Missing or unsupported BEEF data'
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def decode_utf8!(bytes)
|
|
173
|
+
raise 'Nil field' if bytes.nil?
|
|
174
|
+
|
|
175
|
+
str = bytes.force_encoding(Encoding::UTF_8)
|
|
176
|
+
raise 'Invalid UTF-8' unless str.valid_encoding?
|
|
177
|
+
|
|
178
|
+
str
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def decode_protocol_id!(bytes)
|
|
182
|
+
JSON.parse(decode_utf8!(bytes))
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# ---- Signature verification ----
|
|
186
|
+
|
|
187
|
+
def verify_signature!(data_fields, sig_bytes, protocol_id, key, controller)
|
|
188
|
+
@proto_wallet.verify_signature(
|
|
189
|
+
data: data_fields.join.bytes,
|
|
190
|
+
signature: sig_bytes.bytes,
|
|
191
|
+
protocol_id: protocol_id,
|
|
192
|
+
key_id: key,
|
|
193
|
+
counterparty: controller
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# ---- Query key camelisation ----
|
|
198
|
+
|
|
199
|
+
def camelise(query)
|
|
200
|
+
query.transform_keys do |k|
|
|
201
|
+
case k
|
|
202
|
+
when :protocol_id then :protocolID
|
|
203
|
+
when :tag_query_mode then :tagQueryMode
|
|
204
|
+
else k
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module KVStore
|
|
7
|
+
# Historian interpreter for KVStore PushDrop tokens.
|
|
8
|
+
#
|
|
9
|
+
# Decodes a PushDrop locking script from the specified output, validates it
|
|
10
|
+
# has the expected field count (5 old format / 6 new format), and filters by
|
|
11
|
+
# +ctx[:key]+ and +ctx[:protocol_id]+.
|
|
12
|
+
#
|
|
13
|
+
# Returns the value as a UTF-8 string, or +nil+ for any non-match or error.
|
|
14
|
+
# Never raises.
|
|
15
|
+
#
|
|
16
|
+
# Field layout (old format — 5 fields):
|
|
17
|
+
# [0] protocolID (JSON-encoded array)
|
|
18
|
+
# [1] key
|
|
19
|
+
# [2] value
|
|
20
|
+
# [3] controller
|
|
21
|
+
# [4] signature
|
|
22
|
+
#
|
|
23
|
+
# Field layout (new format — 6 fields):
|
|
24
|
+
# [0] protocolID (JSON-encoded array)
|
|
25
|
+
# [1] key
|
|
26
|
+
# [2] value
|
|
27
|
+
# [3] controller
|
|
28
|
+
# [4] tags
|
|
29
|
+
# [5] signature
|
|
30
|
+
module Interpreter
|
|
31
|
+
PROTOCOL_ID = 0
|
|
32
|
+
KEY = 1
|
|
33
|
+
VALUE = 2
|
|
34
|
+
CONTROLLER = 3
|
|
35
|
+
TAGS_NEW = 4
|
|
36
|
+
SIGNATURE_OLD = 4
|
|
37
|
+
SIGNATURE_NEW = 5
|
|
38
|
+
|
|
39
|
+
KV_PROTOCOL_FIELDS_OLD = 5
|
|
40
|
+
KV_PROTOCOL_FIELDS_NEW = 6
|
|
41
|
+
|
|
42
|
+
# Decode the KVStore token at +output_index+ in +tx+.
|
|
43
|
+
#
|
|
44
|
+
# Implements the Historian interpreter contract:
|
|
45
|
+
# +interpreter.call(tx, output_index, ctx)+ → String or nil.
|
|
46
|
+
#
|
|
47
|
+
# @param tx [Transaction::Tx, nil] the transaction to inspect
|
|
48
|
+
# @param output_index [Integer] index into +tx.outputs+
|
|
49
|
+
# @param ctx [Hash, nil] must contain +:key+ (String) and +:protocol_id+ (Array)
|
|
50
|
+
# @return [String, nil] the decoded UTF-8 value, or nil on any mismatch/error
|
|
51
|
+
def self.call(tx, output_index, ctx)
|
|
52
|
+
return nil if tx.nil?
|
|
53
|
+
return nil if ctx.nil? || ctx[:key].nil?
|
|
54
|
+
|
|
55
|
+
output = tx.outputs[output_index]
|
|
56
|
+
return nil if output.nil?
|
|
57
|
+
|
|
58
|
+
locking_script = output.locking_script
|
|
59
|
+
return nil if locking_script.nil?
|
|
60
|
+
|
|
61
|
+
return nil unless locking_script.pushdrop?
|
|
62
|
+
|
|
63
|
+
fields = locking_script.pushdrop_fields
|
|
64
|
+
return nil unless fields
|
|
65
|
+
|
|
66
|
+
field_count = fields.length
|
|
67
|
+
return nil unless [KV_PROTOCOL_FIELDS_OLD, KV_PROTOCOL_FIELDS_NEW].include?(field_count)
|
|
68
|
+
|
|
69
|
+
protocol_id_bytes = fields[PROTOCOL_ID]
|
|
70
|
+
return nil if protocol_id_bytes.nil?
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
protocol_id_str = protocol_id_bytes.force_encoding(Encoding::UTF_8)
|
|
74
|
+
return nil unless protocol_id_str.valid_encoding?
|
|
75
|
+
|
|
76
|
+
parsed_protocol_id = JSON.parse(protocol_id_str)
|
|
77
|
+
return nil unless parsed_protocol_id == ctx[:protocol_id]
|
|
78
|
+
rescue JSON::ParserError
|
|
79
|
+
return nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
key_bytes = fields[KEY]
|
|
83
|
+
return nil if key_bytes.nil?
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
key_str = key_bytes.force_encoding(Encoding::UTF_8)
|
|
87
|
+
return nil unless key_str.valid_encoding?
|
|
88
|
+
return nil unless key_str == ctx[:key]
|
|
89
|
+
rescue StandardError
|
|
90
|
+
return nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
begin
|
|
94
|
+
value_bytes = fields[VALUE]
|
|
95
|
+
return nil if value_bytes.nil?
|
|
96
|
+
|
|
97
|
+
value_str = value_bytes.force_encoding(Encoding::UTF_8)
|
|
98
|
+
return nil unless value_str.valid_encoding?
|
|
99
|
+
|
|
100
|
+
value_str
|
|
101
|
+
rescue StandardError
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
rescue StandardError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module KVStore
|
|
5
|
+
# Transaction output reference for a KVStore token.
|
|
6
|
+
#
|
|
7
|
+
# Populated when +include_token: true+ is passed to {Global#get}.
|
|
8
|
+
Token = Data.define(:dtxid, :output_index, :beef, :satoshis)
|
|
9
|
+
end
|
|
10
|
+
end
|
data/lib/bsv/kv_store.rb
ADDED
|
@@ -21,7 +21,7 @@ module BSV
|
|
|
21
21
|
|
|
22
22
|
# Convert a Transaction to a hash suitable for JSON responses.
|
|
23
23
|
#
|
|
24
|
-
# @param tx [
|
|
24
|
+
# @param tx [Transaction::Tx]
|
|
25
25
|
# @return [Hash]
|
|
26
26
|
def self.transaction_to_h(tx)
|
|
27
27
|
{
|
|
@@ -33,7 +33,7 @@ module BSV
|
|
|
33
33
|
}
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
# Convert a TransactionInput to a hash.
|
|
36
|
+
# Convert a Transaction::TransactionInput to a hash.
|
|
37
37
|
# @api private
|
|
38
38
|
def self.input_to_h(input, _index)
|
|
39
39
|
unlock_script = input.unlocking_script
|
|
@@ -46,7 +46,7 @@ module BSV
|
|
|
46
46
|
}
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
# Convert a TransactionOutput to a hash.
|
|
49
|
+
# Convert a Transaction::TransactionOutput to a hash.
|
|
50
50
|
# @api private
|
|
51
51
|
def self.output_to_h(output, index)
|
|
52
52
|
script = output.locking_script
|
|
@@ -80,7 +80,7 @@ module BSV
|
|
|
80
80
|
# Computes the BIP-143 sighash (SIGHASH_ALL|FORK_ID) and signs it
|
|
81
81
|
# using the wallet's derived key for the protocol.
|
|
82
82
|
#
|
|
83
|
-
# @param tx [
|
|
83
|
+
# @param tx [Transaction::Tx] the spending transaction
|
|
84
84
|
# @param input_index [Integer] which input to sign
|
|
85
85
|
# @return [BSV::Script::Script] the unlocking script
|
|
86
86
|
def sign(tx, input_index)
|
|
@@ -101,7 +101,7 @@ module BSV
|
|
|
101
101
|
|
|
102
102
|
# Estimated byte length of the unlocking script.
|
|
103
103
|
#
|
|
104
|
-
# @param _tx [
|
|
104
|
+
# @param _tx [Transaction::Tx] unused
|
|
105
105
|
# @param _input_index [Integer] unused
|
|
106
106
|
# @return [Integer]
|
|
107
107
|
def estimated_length(_tx, _input_index)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Overlay
|
|
5
|
+
# Builds a chronological history (oldest → newest) of typed values by traversing
|
|
6
|
+
# a transaction's input ancestry and interpreting each output with a provided interpreter.
|
|
7
|
+
#
|
|
8
|
+
# The interpreter contract is: +interpreter.call(tx, output_index, ctx)+, returning the
|
|
9
|
+
# typed value or +nil+. Any callable responding to +:call+ is accepted (lambda, proc,
|
|
10
|
+
# method, or object).
|
|
11
|
+
#
|
|
12
|
+
# Traversal follows +input.source_transaction+ references recursively. Callers must
|
|
13
|
+
# supply transactions whose inputs have +source_transaction+ populated.
|
|
14
|
+
#
|
|
15
|
+
# == Cycle safety
|
|
16
|
+
#
|
|
17
|
+
# Each transaction is visited at most once, tracked by its +wtxid+ (wire-order binary).
|
|
18
|
+
#
|
|
19
|
+
# == Value semantics
|
|
20
|
+
#
|
|
21
|
+
# Only +nil+ is excluded. Falsy non-nil values (+false+, +""+, +0+) are valid history
|
|
22
|
+
# entries and are included in the result.
|
|
23
|
+
#
|
|
24
|
+
# == Caching
|
|
25
|
+
#
|
|
26
|
+
# An optional +history_cache+ (any +[]/[]=+ responder) caches complete history results.
|
|
27
|
+
# Cache keys have the form: <tt>"#{interpreter_version}|#{dtxid_hex}|#{ctx_key}"</tt>.
|
|
28
|
+
# Cached arrays are stored frozen; each retrieval returns a +dup+ to protect the cache.
|
|
29
|
+
#
|
|
30
|
+
# == Note
|
|
31
|
+
#
|
|
32
|
+
# The Ruby Historian is synchronous. There is no async/await semantics.
|
|
33
|
+
class Historian
|
|
34
|
+
# @param interpreter [#call] callable: +call(tx, output_index, ctx) → value|nil+
|
|
35
|
+
# @param debug [Boolean] enable debug logging via +BSV.logger+
|
|
36
|
+
# @param history_cache [Hash, nil] optional cache store (+[]/[]=+ responder)
|
|
37
|
+
# @param interpreter_version [String] version tag for cache key invalidation
|
|
38
|
+
# @param ctx_key_fn [#call, nil] serialises context to a string cache key
|
|
39
|
+
def initialize(interpreter, debug: false, history_cache: nil, interpreter_version: 'v1', ctx_key_fn: nil)
|
|
40
|
+
@interpreter = interpreter
|
|
41
|
+
@debug = debug
|
|
42
|
+
@history_cache = history_cache
|
|
43
|
+
@interpreter_version = interpreter_version
|
|
44
|
+
@ctx_key_fn = ctx_key_fn
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Traverses input ancestry from +start_transaction+ and returns all interpreted
|
|
48
|
+
# values in chronological order (oldest first).
|
|
49
|
+
#
|
|
50
|
+
# @param start_transaction [Transaction::Tx]
|
|
51
|
+
# @param context [Object, nil] forwarded verbatim to the interpreter
|
|
52
|
+
# @return [Array] interpreted values, oldest first
|
|
53
|
+
def build_history(start_transaction, context = nil)
|
|
54
|
+
if @history_cache
|
|
55
|
+
key = cache_key(start_transaction, context)
|
|
56
|
+
cached = @history_cache[key]
|
|
57
|
+
unless cached.nil?
|
|
58
|
+
BSV.logger&.debug { "[Historian] History cache hit: #{key}" } if @debug
|
|
59
|
+
return cached.dup
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
history = []
|
|
64
|
+
visited = {}
|
|
65
|
+
|
|
66
|
+
traverse(start_transaction, context, history, visited)
|
|
67
|
+
|
|
68
|
+
result = history.reverse
|
|
69
|
+
|
|
70
|
+
if @history_cache
|
|
71
|
+
key ||= cache_key(start_transaction, context)
|
|
72
|
+
@history_cache[key] = result.dup.freeze
|
|
73
|
+
BSV.logger&.debug { "[Historian] History cached: #{key}" } if @debug
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
result
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def cache_key(tx, context)
|
|
82
|
+
ctx_key = @ctx_key_fn ? @ctx_key_fn.call(context) : context.to_s
|
|
83
|
+
"#{@interpreter_version}|#{tx.dtxid_hex}|#{ctx_key}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def traverse(tx, context, history, visited)
|
|
87
|
+
id = tx.wtxid
|
|
88
|
+
|
|
89
|
+
if visited.key?(id)
|
|
90
|
+
BSV.logger&.debug { "[Historian] Skipping already visited transaction: #{tx.dtxid_hex}" } if @debug
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
visited[id] = true
|
|
95
|
+
|
|
96
|
+
BSV.logger&.debug { "[Historian] Processing transaction: #{tx.dtxid_hex}" } if @debug
|
|
97
|
+
|
|
98
|
+
tx.outputs.each_with_index do |_output, index|
|
|
99
|
+
value = @interpreter.call(tx, index, context)
|
|
100
|
+
unless value.nil?
|
|
101
|
+
history << value
|
|
102
|
+
BSV.logger&.debug { "[Historian] Added value to history: #{value.inspect}" } if @debug
|
|
103
|
+
end
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
BSV.logger&.debug { "[Historian] Failed to interpret output #{index}: #{e.message}" } if @debug
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
tx.inputs.each do |input|
|
|
109
|
+
if input.source_transaction
|
|
110
|
+
traverse(input.source_transaction, context, history, visited)
|
|
111
|
+
elsif @debug
|
|
112
|
+
BSV.logger&.debug { '[Historian] Input missing sourceTransaction, skipping' }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -74,7 +74,7 @@ module BSV
|
|
|
74
74
|
|
|
75
75
|
# Broadcast a transaction to all interested overlay hosts.
|
|
76
76
|
#
|
|
77
|
-
# @param tx [
|
|
77
|
+
# @param tx [Transaction::Tx] the transaction to broadcast
|
|
78
78
|
# @return [OverlayBroadcastResult]
|
|
79
79
|
def broadcast(tx)
|
|
80
80
|
beef = serialise_beef(tx)
|
data/lib/bsv/overlay.rb
CHANGED
|
@@ -25,5 +25,6 @@ module BSV
|
|
|
25
25
|
autoload :HTTPSBroadcastFacilitator, 'bsv/overlay/broadcast_facilitator'
|
|
26
26
|
autoload :TopicBroadcaster, 'bsv/overlay/topic_broadcaster'
|
|
27
27
|
autoload :SHIPBroadcaster, 'bsv/overlay/topic_broadcaster'
|
|
28
|
+
autoload :Historian, 'bsv/overlay/historian'
|
|
28
29
|
end
|
|
29
30
|
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)
|