|
| 1 | +# Plan: bsv-sdk 0.10.0 — Chronicle + Arcade + Directory Restructure |
| 2 | + |
| 3 | +**Previous release**: bsv-sdk 0.9.0 (cross-SDK compliance rollout) |
| 4 | +**Plan**: `.claude/plans/20260408-v090-compliance-rollout.md` |
| 5 | + |
| 6 | +## Context |
| 7 | + |
| 8 | +The tenth minor release bundles three pillars into a cohesive milestone: |
| 9 | + |
| 10 | +1. **Directory restructure** (cosmetic, already merged — PR #327) |
| 11 | +2. **Chronicle opcode support** (big ticket — implementing BSV's restored opcodes) |
| 12 | +3. **Arcade switch** (infrastructure — Chaintracks chain tracker, default broadcaster, batch broadcast) |
| 13 | + |
| 14 | +0.9.0 shipped "raise first" fail-safes for 8 Chronicle opcodes (F7.1/F7.2). This release replaces those raises with correct implementations. The Arcade work adds a proper chain tracker for SPV verification — the missing link that makes `Beef#verify(chain_tracker)` actually useful out of the box. |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## Pillar 1: Directory Restructure (DONE) |
| 19 | + |
| 20 | +PR #327 merged. Files now live under `gem/bsv-sdk/`, `gem/bsv-wallet/`, `gem/bsv-attest/`. Part of the release narrative; no further work needed. |
| 21 | + |
| 22 | +--- |
| 23 | + |
| 24 | +## Pillar 2: Chronicle Opcodes |
| 25 | + |
| 26 | +### Opcodes to implement |
| 27 | + |
| 28 | +| Opcode | Byte | Category | Semantics | Current state | |
| 29 | +|--------|------|----------|-----------|---------------| |
| 30 | +| OP_SUBSTR | 0xb3 | splice | Pop len, offset, buf → push `buf[offset, len]` | raises UnimplementedOpcode | |
| 31 | +| OP_LEFT | 0xb4 | splice | Pop len, buf → push first len bytes | raises UnimplementedOpcode | |
| 32 | +| OP_RIGHT | 0xb5 | splice | Pop len, buf → push last len bytes | raises UnimplementedOpcode | |
| 33 | +| OP_LSHIFTNUM | 0xb6 | arithmetic | Pop bits, n → push `n << bits` (arithmetic) | raises UnimplementedOpcode | |
| 34 | +| OP_RSHIFTNUM | 0xb7 | arithmetic | Pop bits, n → push `n >> bits` (sign-preserving) | raises UnimplementedOpcode | |
| 35 | +| OP_VER | 0x62 | flow control | Push 4-byte LE tx version | raises UnimplementedOpcode | |
| 36 | +| OP_VERIF | 0x65 | flow control | Pop expected version; match → conditional true | raises UnimplementedOpcode | |
| 37 | +| OP_VERNOTIF | 0x66 | flow control | Pop expected version; no match → conditional true | raises UnimplementedOpcode | |
| 38 | +| OP_2MUL | 0x8d | arithmetic | Pop n → push n×2 | raises DisabledOpcode | |
| 39 | +| OP_2DIV | 0x8e | arithmetic | Pop n → push n÷2 (truncated toward zero) | raises DisabledOpcode | |
| 40 | + |
| 41 | +### Reference implementations |
| 42 | + |
| 43 | +- **Go SDK**: `go-sdk/script/interpreter/chronicle_opcodes_test.go` (~609 lines, ~50 test cases) |
| 44 | +- **TS SDK**: `ts-sdk/src/script/Spend.ts` (implementations), `ts-sdk/src/script/__tests/ChronicleOpcodes.test.ts` (~650 lines, ~80 test cases) |
| 45 | + |
| 46 | +### File changes |
| 47 | + |
| 48 | +**`gem/bsv-sdk/lib/bsv/script/interpreter/error.rb`** |
| 49 | +- Add `MISSING_TX_CONTEXT = :missing_tx_context` error code (for OP_VER without tx) |
| 50 | + |
| 51 | +**`gem/bsv-sdk/lib/bsv/script/interpreter/operations/splice.rb`** |
| 52 | +- Add `op_substr`: pop len (int), offset (int), data (bytes). Validate `offset >= 0 && offset < size && len >= 0 && len <= size - offset`. Push `data.byteslice(offset, len)`. Raise `INVALID_INPUT_LENGTH` on out-of-range. |
| 53 | +- Add `op_left`: pop len (int), data (bytes). Validate `len >= 0 && len <= size`. Push `data.byteslice(0, len)`. Raise on out-of-range. (Matches TS/Go — does NOT clamp to size.) |
| 54 | +- Add `op_right`: pop len (int), data (bytes). Validate `len >= 0 && len <= size`. Push `data.byteslice(size - len, len)`. Raise on out-of-range. |
| 55 | + |
| 56 | +**`gem/bsv-sdk/lib/bsv/script/interpreter/operations/arithmetic.rb`** |
| 57 | +- Add `op_2mul`: pop n (int), push `n * ScriptNumber.new(2)`. Replaces `op_disabled`. |
| 58 | +- Add `op_2div`: pop n (int), push `n / ScriptNumber.new(2)`. Replaces `op_disabled`. |
| 59 | +- Add `op_lshiftnum`: pop bits (int), pop value (int). Validate bits >= 0. Push `ScriptNumber.new(value.value << bits.to_i32)`. Stack memory cap catches oversized results. |
| 60 | +- Add `op_rshiftnum`: pop bits (int), pop value (int). Validate bits >= 0. Push `ScriptNumber.new(value.value >> bits.to_i32)`. Ruby's `Integer#>>` is arithmetic (sign-preserving), which is correct. |
| 61 | +- Remove `op_disabled` method (dead code after OP_2MUL/OP_2DIV restoration). |
| 62 | + |
| 63 | +**`gem/bsv-sdk/lib/bsv/script/interpreter/operations/flow_control.rb`** |
| 64 | +- Add `op_ver`: raise `MISSING_TX_CONTEXT` if `@tx.nil?`. Encode `@tx.version` as 4-byte LE (`[@tx.version].pack('V')`). Push bytes. |
| 65 | +- Add `op_verif`: conditional opcode. In executing branch: pop bytes, compare raw 4-byte LE against `@tx.version`. Match → push `:true` to `@cond_stack`. Mismatch or wrong size → push `:false`. In non-executing branch: push `:false` (nesting tracking, no stack pop). Push `false` to `@else_stack`. |
| 66 | +- Add `op_vernotif`: same as `op_verif` but inverted — match → `:false`, mismatch → `:true`. |
| 67 | +- Add private helper `tx_version_bytes` returning `[@tx.version].pack('V')`. |
| 68 | +- `op_unimplemented` becomes dead code — leave for safety but no dispatch routes to it. |
| 69 | + |
| 70 | +**`gem/bsv-sdk/lib/bsv/script/interpreter/interpreter.rb`** |
| 71 | +- Add `Opcodes::OP_VERIF, Opcodes::OP_VERNOTIF` to `CONDITIONAL_OPCODES` array (they must be dispatched in non-executing branches for nesting tracking). |
| 72 | +- Replace the combined unimplemented `when` clause (lines 185-188) with individual dispatch: |
| 73 | + - `when Opcodes::OP_VER then op_ver` |
| 74 | + - `when Opcodes::OP_VERIF then op_verif` |
| 75 | + - `when Opcodes::OP_VERNOTIF then op_vernotif` |
| 76 | + - `when Opcodes::OP_SUBSTR then op_substr` |
| 77 | + - `when Opcodes::OP_LEFT then op_left` |
| 78 | + - `when Opcodes::OP_RIGHT then op_right` |
| 79 | + - `when Opcodes::OP_LSHIFTNUM then op_lshiftnum` |
| 80 | + - `when Opcodes::OP_RSHIFTNUM then op_rshiftnum` |
| 81 | +- Replace `OP_2MUL, OP_2DIV → op_disabled` (line 229-230) with: |
| 82 | + - `when Opcodes::OP_2MUL then op_2mul` |
| 83 | + - `when Opcodes::OP_2DIV then op_2div` |
| 84 | + |
| 85 | +### Test file |
| 86 | + |
| 87 | +**`spec/bsv/script/interpreter/operations/chronicle_spec.rb`** (new) |
| 88 | + |
| 89 | +~55 test cases ported from Go and TS reference tests, covering: |
| 90 | + |
| 91 | +- **OP_SUBSTR**: "HelloWorld" extraction, single char, full string, out-of-range errors, negative offset/len, empty data |
| 92 | +- **OP_LEFT/OP_RIGHT**: first/last N bytes, zero bytes → empty, full length → whole string, exceeds size → error |
| 93 | +- **OP_LSHIFTNUM/OP_RSHIFTNUM**: 1<<2=4, identity (shift by 0), shift to zero, negative number sign preservation, large shifts |
| 94 | +- **OP_2MUL/OP_2DIV**: basic multiplication/division, zero, negative, truncation |
| 95 | +- **OP_VER**: pushes 4-byte LE version 1, version 2, SIZE check = 4, no tx context → MISSING_TX_CONTEXT |
| 96 | +- **OP_VERIF**: matching version → true branch, non-matching → else branch, non-4-byte item → false, nested conditionals |
| 97 | +- **OP_VERNOTIF**: inverse of OP_VERIF |
| 98 | + |
| 99 | +Also update: |
| 100 | +- `spec/bsv/script/interpreter/hardening_spec.rb` — remove or update the "raise UnimplementedOpcode" tests (they should now test correct execution instead) |
| 101 | +- `spec/bsv/script/interpreter/operations/flow_control_spec.rb` — update OP_VER/VERIF/VERNOTIF tests |
| 102 | +- `spec/bsv/script/interpreter/operations/arithmetic_spec.rb` — remove any `op_disabled` tests for OP_2MUL/OP_2DIV |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +## Pillar 3: Arcade Switch |
| 107 | + |
| 108 | +### New: Chaintracks chain tracker |
| 109 | + |
| 110 | +**`gem/bsv-sdk/lib/bsv/transaction/chain_trackers/chaintracks.rb`** (new, ~90 lines) |
| 111 | + |
| 112 | +Follows `WhatsOnChain` pattern: |
| 113 | + |
| 114 | +```ruby |
| 115 | +class Chaintracks < ChainTracker |
| 116 | + MAINNET_URL = 'https://arcade.gorillapool.io' |
| 117 | + TESTNET_URL = 'https://testnet.arcade.gorillapool.io' |
| 118 | + |
| 119 | + def initialize(url: MAINNET_URL, api_key: nil, http_client: nil) |
| 120 | + def valid_root_for_height?(root, height) |
| 121 | + # GET /chaintracks/v2/header/height/{height} |
| 122 | + # Compare response body 'merkleRoot' with root (case-insensitive) |
| 123 | + end |
| 124 | + def current_height |
| 125 | + # GET /chaintracks/v2/tip |
| 126 | + # Return response body 'height' |
| 127 | + end |
| 128 | +end |
| 129 | +``` |
| 130 | + |
| 131 | +Uses Arcade's Chaintracks v2 API (not the legacy v1 or the /api/v1/chain/merkleroot/verify pattern from some reference SDKs). The v2 endpoints return full block headers with `merkleRoot` field — simpler and more direct. |
| 132 | + |
| 133 | +**`gem/bsv-sdk/lib/bsv/transaction/chain_trackers.rb`** (modify) |
| 134 | +- Add `autoload :Chaintracks, 'bsv/transaction/chain_trackers/chaintracks'` |
| 135 | +- Add `self.default(testnet: false, api_key: nil)` factory returning a `Chaintracks` instance |
| 136 | + |
| 137 | +### Modified: ARC broadcaster |
| 138 | + |
| 139 | +**`gem/bsv-sdk/lib/bsv/network/arc.rb`** (modify) |
| 140 | + |
| 141 | +Three additions: |
| 142 | + |
| 143 | +1. **`self.default(testnet: false, **opts)`** — class method factory |
| 144 | + - Mainnet: `https://arc.gorillapool.io` |
| 145 | + - Testnet: `https://testnet.arc.gorillapool.io` |
| 146 | + - Matches TS/Go/Py default broadcaster pattern |
| 147 | + |
| 148 | +2. **`broadcast_many(txs, wait_for: nil, skip_fee_validation: nil, skip_script_validation: nil)`** — batch broadcast |
| 149 | + - POST to `{@url}/v1/txs` |
| 150 | + - Body: JSON array of `{ rawTx: hex }` (each tx independently EF-fallback encoded) |
| 151 | + - Returns array of `BroadcastResponse` |
| 152 | + - Handles per-tx failure detection (same rejected-status logic as single broadcast) |
| 153 | + |
| 154 | +3. **Skip-validation keyword args** on `broadcast` and `broadcast_many`: |
| 155 | + - `skip_fee_validation: true` → `X-SkipFeeValidation: true` header |
| 156 | + - `skip_script_validation: true` → `X-SkipScriptValidation: true` header |
| 157 | + - Extract header building into private `build_request_headers(wait_for:, skip_fee_validation:, skip_script_validation:)` to avoid duplication |
| 158 | + |
| 159 | +### Test files |
| 160 | + |
| 161 | +**`spec/bsv/transaction/chain_trackers/chaintracks_spec.rb`** (new, ~100 lines) |
| 162 | +- `valid_root_for_height?` true on match, false on mismatch, false on 404, raises ChainProviderError on 5xx |
| 163 | +- `current_height` returns height from tip, raises on failure |
| 164 | +- API key authentication header |
| 165 | +- URL construction for mainnet/testnet |
| 166 | +- `.default` factory method |
| 167 | + |
| 168 | +**`spec/bsv/network/arc_spec.rb`** (modify) |
| 169 | +- `describe '.default'` — mainnet/testnet URL construction, option passthrough |
| 170 | +- `describe '#broadcast_many'` — batch POST to `/v1/txs`, JSON body format, mixed success/failure, EF fallback |
| 171 | +- `describe 'skip-validation headers'` — X-SkipFeeValidation and X-SkipScriptValidation set when requested |
| 172 | + |
| 173 | +### LivePolicy verification |
| 174 | + |
| 175 | +`LivePolicy` already defaults to `https://arc.gorillapool.io` and fetches `/v1/policy`. Arcade serves this endpoint for ARC compatibility. Verify during integration testing that the response shape parses correctly — the Arcade policy response has `miningFeeBytes` and `miningFeeSatoshis` at root level, while the existing `extract_rate` method looks for `policy.fees.miningFee.{satoshis,bytes}`. May need to add a fallback extraction path for the flat structure. |
| 176 | + |
| 177 | +--- |
| 178 | + |
| 179 | +## Sequencing |
| 180 | + |
| 181 | +``` |
| 182 | +Phase 0: Verify 0.9.0 is clean (all specs pass, master is stable) |
| 183 | +
|
| 184 | +Phase 1: Chronicle opcodes (Pillar 2) |
| 185 | + 1a. Error code addition (error.rb) |
| 186 | + 1b. Splice ops: op_substr, op_left, op_right (splice.rb) ─┐ |
| 187 | + 1c. Arithmetic ops: op_2mul, op_2div, op_lshiftnum, │ parallel |
| 188 | + op_rshiftnum (arithmetic.rb) ─┘ |
| 189 | + 1d. Version ops: op_ver, op_verif, op_vernotif (flow_control.rb) |
| 190 | + 1e. Dispatch table rewiring + CONDITIONAL_OPCODES (interpreter.rb) |
| 191 | + 1f. Chronicle test suite (chronicle_spec.rb) + update existing specs |
| 192 | +
|
| 193 | +Phase 2: Arcade switch (Pillar 3) — can run in parallel with Phase 1 |
| 194 | + 2a. Chaintracks chain tracker (chaintracks.rb + spec) |
| 195 | + 2b. ARC default/broadcast_many/skip-validation (arc.rb + spec) |
| 196 | + 2c. ChainTrackers.default factory + LivePolicy verification |
| 197 | +
|
| 198 | +Phase 3: Release |
| 199 | + 3a. Update version.rb to 0.10.0 |
| 200 | + 3b. CHANGELOG.md with Chronicle and Arcade sections |
| 201 | + 3c. Full test suite pass + RuboCop |
| 202 | +``` |
| 203 | + |
| 204 | +Phases 1 and 2 are independent — no cross-dependencies. |
| 205 | + |
| 206 | +--- |
| 207 | + |
| 208 | +## HLR structure |
| 209 | + |
| 210 | +Two HLRs (Pillar 1 is already done): |
| 211 | + |
| 212 | +- **HLR: Chronicle opcode implementation** — F7.1/F7.2 full semantics. Label `project:hlr`. Covers all 10 opcodes + test suite. |
| 213 | +- **HLR: Arcade integration** — Chaintracks chain tracker, default broadcaster, batch broadcast. Label `project:hlr`. |
| 214 | + |
| 215 | +--- |
| 216 | + |
| 217 | +## Release criteria |
| 218 | + |
| 219 | +- [ ] All existing specs pass (zero regressions) |
| 220 | +- [ ] All 10 Chronicle opcodes execute correctly (~55 new test cases) |
| 221 | +- [ ] Chaintracks chain tracker works with mock HTTP client (~15 new test cases) |
| 222 | +- [ ] ARC.default, broadcast_many, skip-validation work (~10 new test cases) |
| 223 | +- [ ] No `op_unimplemented` calls remain in dispatch table |
| 224 | +- [ ] No `op_disabled` calls remain in dispatch table |
| 225 | +- [ ] `CONDITIONAL_OPCODES` includes OP_VERIF and OP_VERNOTIF |
| 226 | +- [ ] RuboCop clean |
| 227 | +- [ ] CHANGELOG updated |
| 228 | +- [ ] Version = 0.10.0 |
| 229 | + |
| 230 | +--- |
| 231 | + |
| 232 | +## Key files |
| 233 | + |
| 234 | +### Chronicle (modify) |
| 235 | +- `gem/bsv-sdk/lib/bsv/script/interpreter/error.rb` |
| 236 | +- `gem/bsv-sdk/lib/bsv/script/interpreter/interpreter.rb` |
| 237 | +- `gem/bsv-sdk/lib/bsv/script/interpreter/operations/flow_control.rb` |
| 238 | +- `gem/bsv-sdk/lib/bsv/script/interpreter/operations/splice.rb` |
| 239 | +- `gem/bsv-sdk/lib/bsv/script/interpreter/operations/arithmetic.rb` |
| 240 | + |
| 241 | +### Chronicle (new) |
| 242 | +- `spec/bsv/script/interpreter/operations/chronicle_spec.rb` |
| 243 | + |
| 244 | +### Arcade (modify) |
| 245 | +- `gem/bsv-sdk/lib/bsv/network/arc.rb` |
| 246 | +- `gem/bsv-sdk/lib/bsv/transaction/chain_trackers.rb` |
| 247 | + |
| 248 | +### Arcade (new) |
| 249 | +- `gem/bsv-sdk/lib/bsv/transaction/chain_trackers/chaintracks.rb` |
| 250 | +- `spec/bsv/transaction/chain_trackers/chaintracks_spec.rb` |
| 251 | + |
| 252 | +### Release |
| 253 | +- `gem/bsv-sdk/lib/bsv/version.rb` |
| 254 | +- `CHANGELOG.md` |
0 commit comments