Skip to content

Commit e16db88

Browse files
authored
Merge pull request #32 from sgbett/task/7-attest-gem
feat(attest): add bsv-attest data attestation gem
2 parents ccc1901 + ac4aa9a commit e16db88

File tree

15 files changed

+618
-2
lines changed

15 files changed

+618
-2
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# BSV Attest — Data Attestation Gem
2+
3+
## Context
4+
5+
Issue #7. All SDK dependencies are complete (primitives, script, transaction, network, chain provider, wallet). `bsv-attest` is a thin domain layer: hash data, publish hash to chain, verify hash on chain.
6+
7+
Separate gem (`bsv-attest`) in the same monorepo, depending on `bsv-sdk`.
8+
9+
---
10+
11+
## File Structure
12+
13+
```
14+
bsv-attest.gemspec # NEW
15+
lib/bsv-attest.rb # NEW: gem entry point
16+
lib/bsv/attest.rb # NEW: autoload hub + module methods
17+
lib/bsv/attest/version.rb # NEW
18+
lib/bsv/attest/configuration.rb # NEW
19+
lib/bsv/attest/response.rb # NEW
20+
lib/bsv/attest/verification_error.rb # NEW
21+
22+
spec/bsv/attest/configuration_spec.rb # NEW
23+
spec/bsv/attest/response_spec.rb # NEW
24+
spec/bsv/attest/verification_error_spec.rb # NEW
25+
spec/bsv/attest/attest_spec.rb # NEW
26+
```
27+
28+
**Modified:** `Gemfile` (add second gemspec), `.rubocop.yml` (add attest exclusions)
29+
30+
---
31+
32+
## Build Order (6 steps)
33+
34+
### 1. Version + Gemspec
35+
36+
**`lib/bsv/attest/version.rb`**`BSV::Attest::VERSION = '0.1.0'`
37+
38+
**`bsv-attest.gemspec`** — follows `bsv-sdk.gemspec` pattern:
39+
- `add_dependency 'bsv-sdk'`
40+
- `required_ruby_version >= 2.7`
41+
- `licence: 'Open BSV'`
42+
- `files: Dir.glob('lib/bsv/attest{.rb,/**/*}') + %w[lib/bsv-attest.rb LICENCE]`
43+
44+
### 2. VerificationError
45+
46+
**`lib/bsv/attest/verification_error.rb`**
47+
48+
```ruby
49+
class VerificationError < StandardError; end
50+
```
51+
52+
**Spec:** StandardError subclass, stores message.
53+
54+
### 3. Configuration
55+
56+
**`lib/bsv/attest/configuration.rb`**
57+
58+
```ruby
59+
class Configuration
60+
attr_accessor :wallet, :broadcaster, :provider
61+
end
62+
```
63+
64+
- `wallet``BSV::Wallet::Wallet` instance (for fund+sign)
65+
- `broadcaster` — any `#broadcast(tx)` (e.g. ARC)
66+
- `provider` — any `#fetch_transaction(txid)` (e.g. WhatsOnChain, for verify)
67+
- All default to `nil`; errors surface at use time
68+
69+
**Spec:** defaults to nil, supports setting all three attributes.
70+
71+
### 4. Response
72+
73+
**`lib/bsv/attest/response.rb`**
74+
75+
```ruby
76+
class Response
77+
attr_reader :hash, :transaction, :txid
78+
79+
def initialize(hash:, transaction:, txid:)
80+
def hash_hex # @hash.unpack1('H*')
81+
end
82+
```
83+
84+
- `hash` — binary 32-byte SHA-256 digest
85+
- `transaction``BSV::Transaction::Transaction` instance
86+
- `txid` — hex string from broadcast response
87+
88+
**Spec:** attribute storage, `hash_hex` returns 64-char hex string.
89+
90+
### 5. Module methods + entry point
91+
92+
**`lib/bsv/attest.rb`** — autoload hub + class methods:
93+
94+
```ruby
95+
module BSV
96+
module Attest
97+
autoload :Configuration, 'bsv/attest/configuration'
98+
autoload :Response, 'bsv/attest/response'
99+
autoload :VerificationError, 'bsv/attest/verification_error'
100+
101+
class << self
102+
def configuration # lazy-initialised Configuration.new
103+
def configure # yield(configuration)
104+
def reset_configuration!
105+
106+
def hash(data)
107+
BSV::Primitives::Digest.sha256(data)
108+
end
109+
110+
def publish(data, wallet: nil, broadcaster: nil)
111+
# 1. hash(data)
112+
# 2. Build tx with OP_RETURN output containing hash
113+
# 3. wallet.fund_and_sign(tx)
114+
# 4. broadcaster.broadcast(tx)
115+
# 5. Return Response.new(hash:, transaction:, txid:)
116+
# Raises ArgumentError if wallet/broadcaster missing
117+
end
118+
119+
def verify(data, txid, provider: nil)
120+
# 1. hash(data)
121+
# 2. provider.fetch_transaction(txid)
122+
# 3. Scan outputs for OP_FALSE OP_RETURN <data> pattern
123+
# 4. Return true if any data chunk matches hash
124+
# 5. Raise VerificationError if not found
125+
# Raises ArgumentError if provider missing
126+
end
127+
end
128+
end
129+
end
130+
```
131+
132+
**`lib/bsv-attest.rb`** — entry point:
133+
```ruby
134+
require 'bsv-sdk'
135+
require_relative 'bsv/attest'
136+
```
137+
138+
**Key details:**
139+
- `publish` and `verify` accept per-call overrides (`wallet:`, `broadcaster:`, `provider:`) alongside global config
140+
- OP_RETURN detection: `chunks[0].opcode == OP_FALSE && chunks[1].opcode == OP_RETURN`, data from `chunks[2..]`
141+
- Underlying errors propagate naturally (BroadcastError, InsufficientFundsError, ChainProviderError)
142+
- Does NOT add `autoload :Attest` to `lib/bsv-sdk.rb` — separate gem loads itself
143+
144+
**Spec (`attest_spec.rb`):**
145+
- `.hash` — returns 32-byte binary SHA-256, matches `Digest.sha256`
146+
- `.configure` / `.reset_configuration!` — yields config, resets to defaults
147+
- `.publish` — builds OP_RETURN tx, calls fund_and_sign, broadcasts, returns Response; raises ArgumentError without wallet/broadcaster; accepts per-call overrides
148+
- `.verify` — returns true when hash found in OP_RETURN; raises VerificationError when not found; raises ArgumentError without provider; works with multi-push OP_RETURN outputs
149+
150+
**Mock strategy** (follows `wallet_spec.rb` pattern):
151+
- Mock wallet: `#fund_and_sign(tx)` returns tx
152+
- Mock broadcaster: `#broadcast(tx)` returns `BroadcastResponse.new(txid: ...)`
153+
- Mock provider: `#fetch_transaction(txid)` returns a real Transaction built with `Script.op_return`
154+
155+
### 6. Wiring
156+
157+
- **`Gemfile`** — replace placeholder comment with `gemspec name: 'bsv-attest'`
158+
- **`.rubocop.yml`** — add `lib/bsv/attest/**/*` to Metrics exclusions, `spec/bsv/attest/**/*` to RSpec exclusions, `lib/bsv-attest.rb` to `Naming/FileName` exclude
159+
160+
---
161+
162+
## Existing Code to Reuse
163+
164+
| Need | Existing code | File |
165+
|------|--------------|------|
166+
| SHA-256 | `BSV::Primitives::Digest.sha256(data)` | `lib/bsv/primitives/digest.rb` |
167+
| OP_RETURN script | `BSV::Script::Script.op_return(*data_items)` | `lib/bsv/script/script.rb` |
168+
| Script chunks | `script.chunks``[Chunk]` with `.opcode`, `.data` | `lib/bsv/script/chunk.rb` |
169+
| Opcodes | `OP_FALSE = 0x00`, `OP_RETURN = 0x6a` | `lib/bsv/script/opcodes.rb` |
170+
| Transaction | `BSV::Transaction::Transaction.new`, `.add_output` | `lib/bsv/transaction/transaction.rb` |
171+
| Output | `TransactionOutput.new(satoshis:, locking_script:)` | `lib/bsv/transaction/transaction_output.rb` |
172+
| Fund+sign | `wallet.fund_and_sign(tx)` | `lib/bsv/wallet/wallet.rb` |
173+
| Broadcast | `broadcaster.broadcast(tx)``BroadcastResponse` | `lib/bsv/network/arc.rb` |
174+
| Fetch tx | `provider.fetch_transaction(txid)``Transaction` | `lib/bsv/network/whats_on_chain.rb` |
175+
176+
---
177+
178+
## Commit Sequence
179+
180+
1. `feat(attest): add bsv-attest gem with version and gemspec`
181+
2. `feat(attest): add VerificationError exception class`
182+
3. `feat(attest): add Configuration class`
183+
4. `feat(attest): add Response value object`
184+
5. `feat(attest): add hash, publish, and verify module methods`
185+
6. `chore(attest): wire up Gemfile and extend RuboCop exclusions`
186+
187+
---
188+
189+
## Verification
190+
191+
```bash
192+
bundle install # resolves both gemspecs
193+
bundle exec rspec spec/bsv/attest/ # attest specs pass
194+
bundle exec rubocop # no new lint violations
195+
bundle exec rake # full suite green
196+
```

.rubocop.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Metrics/BlockLength:
1616
Naming/FileName:
1717
Exclude:
1818
- 'lib/bsv-sdk.rb'
19+
- 'lib/bsv-attest.rb'
1920

2021
# Cryptographic code uses standard short names (r, s, bn, etc.)
2122
Naming/MethodParameterName:
@@ -39,6 +40,8 @@ Metrics/AbcSize:
3940
- 'lib/bsv/transaction/**/*'
4041
- 'lib/bsv/network/**/*'
4142
- 'lib/bsv/wallet/**/*'
43+
- 'lib/bsv/attest.rb'
44+
- 'lib/bsv/attest/**/*'
4245

4346
Metrics/MethodLength:
4447
Exclude:
@@ -48,6 +51,8 @@ Metrics/MethodLength:
4851
- 'lib/bsv/network/**/*'
4952
- 'lib/bsv/wallet/**/*'
5053
- 'spec/support/**/*'
54+
- 'lib/bsv/attest.rb'
55+
- 'lib/bsv/attest/**/*'
5156

5257
Metrics/CyclomaticComplexity:
5358
Exclude:
@@ -56,6 +61,8 @@ Metrics/CyclomaticComplexity:
5661
- 'lib/bsv/transaction/**/*'
5762
- 'lib/bsv/network/**/*'
5863
- 'lib/bsv/wallet/**/*'
64+
- 'lib/bsv/attest.rb'
65+
- 'lib/bsv/attest/**/*'
5966

6067
Metrics/PerceivedComplexity:
6168
Exclude:
@@ -64,6 +71,8 @@ Metrics/PerceivedComplexity:
6471
- 'lib/bsv/transaction/**/*'
6572
- 'lib/bsv/network/**/*'
6673
- 'lib/bsv/wallet/**/*'
74+
- 'lib/bsv/attest.rb'
75+
- 'lib/bsv/attest/**/*'
6776

6877
Metrics/ModuleLength:
6978
Exclude:
@@ -83,6 +92,8 @@ RSpec/MultipleExpectations:
8392
- 'spec/bsv/transaction/**/*'
8493
- 'spec/bsv/network/**/*'
8594
- 'spec/bsv/wallet/**/*'
95+
- 'spec/bsv/attest_spec.rb'
96+
- 'spec/bsv/attest/**/*'
8697
- 'spec/integration/**/*'
8798

8899
RSpec/ExampleLength:
@@ -92,6 +103,8 @@ RSpec/ExampleLength:
92103
- 'spec/bsv/transaction/**/*'
93104
- 'spec/bsv/network/**/*'
94105
- 'spec/bsv/wallet/**/*'
106+
- 'spec/bsv/attest_spec.rb'
107+
- 'spec/bsv/attest/**/*'
95108
- 'spec/integration/**/*'
96109

97110
# Integration tests use before(:context) which requires instance variables.

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
source 'https://rubygems.org'
44

55
gemspec name: 'bsv-sdk'
6-
# add more later: gemspec name: "bsv-wallet" etc.
6+
gemspec name: 'bsv-attest'
77

88
group :development, :test do
99
gem 'rake'

Rakefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

3-
require 'bundler/gem_tasks'
3+
Bundler::GemHelper.install_tasks(name: 'bsv-sdk')
4+
Bundler::GemHelper.install_tasks(name: 'bsv-attest')
45
require 'rspec/core/rake_task'
56

67
RSpec::Core::RakeTask.new(:spec)

bsv-attest.gemspec

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'lib/bsv/attest/version'
4+
5+
Gem::Specification.new do |spec|
6+
spec.name = 'bsv-attest'
7+
spec.version = BSV::Attest::VERSION
8+
spec.authors = ['Simon Bettison']
9+
10+
spec.summary = 'Data attestation for the BSV Blockchain'
11+
spec.description = 'Hash data, publish hashes to the BSV blockchain via OP_RETURN, ' \
12+
'and verify attestations on chain.'
13+
spec.homepage = 'https://github.com/sgbett/bsv-ruby-sdk'
14+
spec.license = 'Open BSV'
15+
16+
spec.required_ruby_version = '>= 2.7'
17+
18+
spec.metadata = {
19+
'homepage_uri' => spec.homepage,
20+
'source_code_uri' => spec.homepage,
21+
'changelog_uri' => "#{spec.homepage}/blob/master/CHANGELOG.md",
22+
'rubygems_mfa_required' => 'true'
23+
}
24+
25+
spec.files = Dir.glob('lib/bsv/attest{.rb,/**/*}') + %w[lib/bsv-attest.rb LICENCE]
26+
spec.require_paths = ['lib']
27+
28+
spec.add_dependency 'bsv-sdk'
29+
end

lib/bsv-attest.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# frozen_string_literal: true
2+
3+
require 'bsv-sdk'
4+
require_relative 'bsv/attest'

lib/bsv/attest.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
module BSV
4+
module Attest
5+
autoload :Configuration, 'bsv/attest/configuration'
6+
autoload :Response, 'bsv/attest/response'
7+
autoload :VerificationError, 'bsv/attest/verification_error'
8+
autoload :VERSION, 'bsv/attest/version'
9+
10+
class << self
11+
def configuration
12+
@configuration ||= Configuration.new
13+
end
14+
15+
def configure
16+
yield(configuration)
17+
end
18+
19+
def reset_configuration!
20+
@configuration = Configuration.new
21+
end
22+
23+
def hash(data)
24+
BSV::Primitives::Digest.sha256(data)
25+
end
26+
27+
def publish(data, wallet: nil, broadcaster: nil)
28+
w = wallet || configuration.wallet
29+
b = broadcaster || configuration.broadcaster
30+
raise ArgumentError, 'wallet is required' unless w
31+
raise ArgumentError, 'broadcaster is required' unless b
32+
33+
digest = hash(data)
34+
35+
script = BSV::Script::Script.op_return(digest)
36+
output = BSV::Transaction::TransactionOutput.new(satoshis: 0, locking_script: script)
37+
38+
tx = BSV::Transaction::Transaction.new
39+
tx.add_output(output)
40+
41+
w.fund_and_sign(tx)
42+
43+
broadcast_response = b.broadcast(tx)
44+
45+
Response.new(hash: digest, transaction: tx, txid: broadcast_response.txid)
46+
end
47+
48+
def verify(data, txid, provider: nil)
49+
p = provider || configuration.provider
50+
raise ArgumentError, 'provider is required' unless p
51+
52+
digest = hash(data)
53+
54+
tx = p.fetch_transaction(txid)
55+
56+
tx.outputs.each do |output|
57+
chunks = output.locking_script.chunks
58+
next if chunks.length < 3
59+
next unless chunks[0].opcode == BSV::Script::Opcodes::OP_FALSE
60+
next unless chunks[1].opcode == BSV::Script::Opcodes::OP_RETURN
61+
62+
chunks[2..].each do |chunk|
63+
return true if chunk.data == digest
64+
end
65+
end
66+
67+
raise VerificationError, 'hash not found in transaction outputs'
68+
end
69+
end
70+
end
71+
end

lib/bsv/attest/configuration.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
module BSV
4+
module Attest
5+
class Configuration
6+
attr_accessor :wallet, :broadcaster, :provider
7+
end
8+
end
9+
end

0 commit comments

Comments
 (0)