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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6152fa9bc473f3ff11eca96fa5a9f1302d88cdbe3d72782c13da1760a5507514
|
|
4
|
+
data.tar.gz: e3f45fa1d03d36f9943d172fb8db396869f2ad2ff333b8d071af161062e56fc3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 802d2a0fe86432760fa64b12ac6787b4e8a9c3ce02ea5f66e19eee9e3b61bc050f0712fa8c98f5cfe0ca92550a7b7df7927624f5b120fb5274aac082acb66a0b
|
|
7
|
+
data.tar.gz: 7ecae6d4a729130f3d14e637b5822cec7bcab141a7e32abcd4d0275b4c81a0c545d64a2272a8684ef8702b85f4a676f6e3545437ccca5f38a1b57108099baf57
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,68 @@ 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.26.0 — 2026-07-08
|
|
9
|
+
|
|
10
|
+
### Breaking Changes
|
|
11
|
+
- `Transaction::Tx#verify` — the `verified:` kwarg now accepts `nil | Hash{String => Boolean}`
|
|
12
|
+
(previously `nil | Set<String>`). The Hash is mutated in place, so a caller can both pre-seed
|
|
13
|
+
the already-verified wtxid set and read back the wtxids walked during verification. A frozen
|
|
14
|
+
Hash, or a Hash with a truthy `default`/`default_proc`, is rejected at entry (funds risk) (#904).
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `Transaction::Tx#verify(verified:)` bidirectional verification cache: pre-seeding short-circuits
|
|
18
|
+
already-verified ancestor subtrees; post-read exposes the walked wtxid set for a persistent
|
|
19
|
+
verification cache (#904, #909, #910).
|
|
20
|
+
- Transaction memoisation: per-struct binary, component-hash, and wire-order memos across `Tx`,
|
|
21
|
+
inputs, and outputs, invalidated on mutation — repeated `to_binary`/`wtxid`/hash computation is
|
|
22
|
+
now cached (#881).
|
|
23
|
+
- Conformance canonical corpus: a pinned-revision loader plus fetch/sync tooling and cache layout,
|
|
24
|
+
with the BEEF, BUMP, sighash, SHA-256, ECIES, BRC-42 key-derivation, and script-test specs
|
|
25
|
+
migrated onto it (#840, #841, #842–#847).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Interpreter consensus hardening: correct Chronicle gating of `OP_VER`/`OP_VERIF` (with
|
|
29
|
+
tx_version), `OP_2MUL`/`OP_2DIV`, and multi-`OP_ELSE` (relaxed mode); enforce the `CLEANSTACK`
|
|
30
|
+
and `SIGPUSHONLY` verification flags; whitelist verification flags (#837, #850, #851, #852, #853,
|
|
31
|
+
#854).
|
|
32
|
+
- `Transaction::Tx#verify` — translate the fee-gate `ArgumentError` (missing source data) into a
|
|
33
|
+
`VerificationError` (`:missing_source`) so every failure mode raises a consistent error type
|
|
34
|
+
(#904).
|
|
35
|
+
- `Transaction::Tx#verify` — fix a funds-risk bug where a Hash with a truthy default could silently
|
|
36
|
+
short-circuit verification for unseen wtxids (#904).
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- Performance: reuse OpenSSL digest contexts in primitives hashing (thread-safety documented); split
|
|
40
|
+
`clear_caches` from `invalidate_caches` in the transaction cache lifecycle (#881, #882).
|
|
41
|
+
|
|
42
|
+
## 0.25.0 — 2026-06-18
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- `BSV::Storage::Utils` — UHRP URL helpers (`normalise_url`, `url_for_hash`,
|
|
46
|
+
`url_for_file`, `hash_from_url`, `valid_url?`) (#817).
|
|
47
|
+
- `BSV::Overlay::Historian` — generic output-history walker over `LookupResolver`,
|
|
48
|
+
enabling protocol-specific replay of an outpoint's lineage (#818).
|
|
49
|
+
- `BSV::Registry` — typed `resolve_basket`, `resolve_protocol`, and
|
|
50
|
+
`resolve_certificate` methods on the registry client (#819).
|
|
51
|
+
- `BSV::KVStore::Interpreter` — `Historian`-compatible interpreter for KV
|
|
52
|
+
protocol outputs (#820).
|
|
53
|
+
- `BSV::Storage::Downloader` — UHRP resolver and content fetcher with
|
|
54
|
+
hash verification (#821).
|
|
55
|
+
- `BSV::KVStore::Global` — overlay-backed read-only key-value reader (#822).
|
|
56
|
+
- `BSV::Script::BIP276` — text encoding for scripts and templates (`encode`,
|
|
57
|
+
`decode`) (#830).
|
|
58
|
+
- `BSV::Transaction::BeefParty` — `Beef` subclass tracking per-party knowledge
|
|
59
|
+
of transactions for multi-party BEEF exchange, plus supporting methods on
|
|
60
|
+
`Beef` (#831).
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
- `Beef` wire-inputs debug log now uses the existing `TransactionInput#dtxid_hex`
|
|
64
|
+
and `Tx#dtxid` accessors instead of inlined byte-order conversions (#828).
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
- ECIES bitcore variant — added `iv:` kwarg and TypeScript SDK conformance
|
|
68
|
+
vector (#829).
|
|
69
|
+
|
|
8
70
|
## 0.24.0 — 2026-06-11
|
|
9
71
|
|
|
10
72
|
### Breaking Changes
|
data/README.md
CHANGED
|
@@ -24,7 +24,7 @@ This Ruby SDK is a port of the official BSV Blockchain SDKs, which serve as its
|
|
|
24
24
|
|
|
25
25
|
The reference SDKs:
|
|
26
26
|
|
|
27
|
-
- [TypeScript SDK](https://github.com/bsv-blockchain/ts-
|
|
27
|
+
- [TypeScript SDK](https://github.com/bsv-blockchain/ts-stack)
|
|
28
28
|
- [Go SDK](https://github.com/bsv-blockchain/go-sdk)
|
|
29
29
|
- [Python SDK](https://github.com/bsv-blockchain/py-sdk)
|
|
30
30
|
|
|
@@ -89,8 +89,10 @@ locking_script = BSV::Script::Script.p2pkh_lock(pubkey_hash)
|
|
|
89
89
|
tx = BSV::Transaction::Tx.new
|
|
90
90
|
|
|
91
91
|
# Add an input referencing a previous transaction output
|
|
92
|
+
# wtxid_from_hex converts a display-order hex txid to wire-order binary
|
|
93
|
+
utxo_txid = '5884e5db9de218238671572340b207ee85b628074e7e467096c267266baf77a4'
|
|
92
94
|
input = BSV::Transaction::TransactionInput.new(
|
|
93
|
-
|
|
95
|
+
prev_wtxid: BSV::Transaction::TransactionInput.wtxid_from_hex(utxo_txid),
|
|
94
96
|
prev_tx_out_index: 0
|
|
95
97
|
)
|
|
96
98
|
input.source_satoshis = 100_000
|
|
@@ -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
|