|
| 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 | +``` |
0 commit comments