x402-rack 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bd77486c75afc23677145007680438a0a261f168a6314bc5e8038de1429d51b0
4
+ data.tar.gz: ce8eaeff559f9a7f376991fc33c4233d3c85883e4f71000c4ab609e15fd60eb8
5
+ SHA512:
6
+ metadata.gz: 9b692baf1bfc107e95dabeed52cb672447e578b25d7d135a2983d9caa61c719ab9b0e4552bf5ea57ccb28d9f0a06b2ab873ced1bbb8630c18d84e63452e2d992
7
+ data.tar.gz: ecec6258c8b30604a33f4894af7dec5477e929eafc2340a0de88417e2a89f56bb3b8469934c4087c90568f6b9559d89f803cde8ff17aea537aded30df9ed5d26
@@ -0,0 +1,384 @@
1
+ # Plan: X402::BSV Module — Responsibilities and Interface
2
+
3
+ ## Context
4
+
5
+ The codebase has BSV-specific logic in `X402::Verification::SettlementChecks` that needs to move behind an `X402::BSV` boundary. Through analysis of the merkleworks reference implementation (including Profile B settlement flow), the Coinbase x402 ecosystem, and direct discussion with Rui at merkleworks, the architecture has clarified into clean component boundaries.
6
+
7
+ ### Ecosystem landscape
8
+
9
+ Two x402 ecosystems exist with different conventions:
10
+
11
+ **Coinbase x402 v2** (broad ecosystem):
12
+ - Headers: `Payment-Required` / `Payment-Signature` / `Payment-Response`
13
+ - Client signs authorisation, facilitator broadcasts (verify → serve → settle)
14
+ - No server-provided nonces — replay protection is chain-native (EIP-712, etc.)
15
+ - No request binding beyond resource URL
16
+ - `accepts` array in challenge for multi-chain/multi-scheme negotiation
17
+
18
+ **Merkleworks x402** (BSV-specific):
19
+ - Headers: `X402-Challenge` / `X402-Proof`
20
+ - Client broadcasts, server checks mempool (proof-of-payment model)
21
+ - Server-provided 1-sat nonce UTXO for challenge binding + replay protection
22
+ - Strong request binding (method, path, query, headers hash, body hash)
23
+ - Single-chain, single-scheme
24
+
25
+ **Our position**: Our PayGateway implements the Coinbase v2 header spec (`Payment-Required` / `Payment-Signature` / `Payment-Response`) with BSV as the settlement network. This makes BSV a first-class citizen in the broader x402 ecosystem — any Coinbase-compatible server or client can interoperate with us. We also support merkleworks via a separate ProofGateway that uses the `X402-*` headers. The `X402-*` headers are merkleworks' territory; we speak the standard x402 v2 language for our own scheme.
26
+
27
+ ## Component Boundaries
28
+
29
+ ### Gatekeeper (`X402::Middleware`)
30
+
31
+ The Rack middleware. A pure dispatcher with no blockchain knowledge. It:
32
+
33
+ 1. **On request**: polls each configured gateway for challenge headers, returns all of them in the 402 response
34
+ 2. **On proof**: checks which proof/payment header the client sent, dispatches to the matching gateway
35
+ 3. **On result**: the gateway returns allow/deny — the middleware serves or rejects accordingly
36
+
37
+ The gatekeeper **MUST NOT sign transactions or hold private keys** (Invariant A-2 from the merkleworks spec). It manages HTTP headers, route matching, and dispatch. Everything else is the gateway's job.
38
+
39
+ ### Gateway (`X402::BSV::ProofGateway`, `X402::BSV::PayGateway`)
40
+
41
+ The bridge between the gatekeeper and the BSV network. Gateways **can** hold keys and sign transactions — they are separate components from the gatekeeper. Each gateway:
42
+
43
+ - Builds challenge data (including partial transaction templates)
44
+ - Verifies and settles proofs
45
+ - Interacts with ARC and/or a treasury service
46
+
47
+ ### Treasury (external service or local key)
48
+
49
+ Holds the nonce key, mints nonce UTXOs. The ProofGateway either talks to an external treasury service (`wallet_url`) or holds a key directly (`nonce_key`). The PayGateway doesn't need a treasury.
50
+
51
+ ### Delegator (separate service, not our concern)
52
+
53
+ Adds fee inputs to client-constructed partial txs, signs only its fee inputs. Lives between the client and the network. The gatekeeper and gateways never talk to it.
54
+
55
+ ## Architecture: Unified Template Model
56
+
57
+ ### The key insight: both gateways produce partial transaction templates
58
+
59
+ The challenge is not just metadata — it includes a **partial transaction template** that the client extends by adding funding inputs (and optionally change outputs).
60
+
61
+ **Base behaviour** (all gateways): build a partial tx with the payment output (amount to payee). This is the invoice.
62
+
63
+ **ProofGateway override**: prepend the nonce UTXO input at index 0, signed with `SIGHASH_SINGLE | ANYONECANPAY | FORKID (0xC3)`. This locks the payment output (at output index 0) while allowing the client to append inputs and outputs freely. Then add the payment output.
64
+
65
+ **PayGateway**: just the base behaviour. Payment output, no nonce.
66
+
67
+ ```ruby
68
+ # Pseudocode
69
+ class Gateway
70
+ def build_template(route, config)
71
+ tx = Transaction.new
72
+ tx.add_output(payee: config.payee_locking_script_hex, amount: route.amount_sats)
73
+ tx
74
+ end
75
+ end
76
+
77
+ class ProofGateway < Gateway
78
+ def build_template(route, config)
79
+ tx = Transaction.new
80
+ nonce = fetch_nonce # from treasury
81
+ # Input 0: nonce UTXO, signed with 0xC3
82
+ # SIGHASH_SINGLE commits to output[0] only
83
+ # ANYONECANPAY allows additional inputs
84
+ tx.add_signed_input(nonce, key: @nonce_key, sighash: 0xC3)
85
+ # Output 0: payment (locked by the 0xC3 signature)
86
+ tx.add_output(payee: config.payee_locking_script_hex, amount: route.amount_sats)
87
+ tx
88
+ end
89
+ end
90
+
91
+ class PayGateway < Gateway
92
+ # inherits build_template — just the payment output, no nonce
93
+ end
94
+ ```
95
+
96
+ The client's job is identical regardless of scheme: add funding inputs, sign them, and either broadcast (BSV-proof) or hand to the server (BSV-pay). The delegator fits the same way in both flows — it receives the extended template and appends fee inputs.
97
+
98
+ ### Why 0xC3 for the nonce signature
99
+
100
+ `SIGHASH_SINGLE | ANYONECANPAY | FORKID`:
101
+ - `SIGHASH_SINGLE`: commits only to `output[input_index]` — since the nonce is at input 0, the signature protects only output 0 (the payment). Additional outputs (change, etc.) can be added freely.
102
+ - `ANYONECANPAY`: excludes other inputs — funding inputs and fee inputs can be appended without invalidating the gateway's signature.
103
+ - `FORKID`: BSV fork ID flag (required).
104
+
105
+ Using `0xC1` (`SIGHASH_ALL | ANYONECANPAY`) would commit to ALL outputs, breaking the extensibility model.
106
+
107
+ ## Multi-protocol Support
108
+
109
+ ### HTTP headers
110
+
111
+ | Scheme | Challenge header | Proof header | Receipt header |
112
+ |--------|-----------------|--------------|----------------|
113
+ | BSV-pay (ours) | `Payment-Required` | `Payment-Signature` | `Payment-Response` |
114
+ | BSV-proof (merkleworks) | `X402-Challenge` | `X402-Proof` | — |
115
+
116
+ Our PayGateway uses the Coinbase v2 headers — the standard x402 ecosystem language. The ProofGateway uses the merkleworks `X402-*` headers. A server running both sends both:
117
+
118
+ ```
119
+ HTTP/1.1 402 Payment Required
120
+ Payment-Required: <base64(v2 PaymentRequired JSON with BSV in accepts)>
121
+ X402-Challenge: <base64url(merkleworks challenge JSON)>
122
+ ```
123
+
124
+ The `Payment-Required` challenge follows the Coinbase v2 structure. The `extra` field carries our BSV-specific enhancements — specifically the partial tx template built by `Gateway.build_template`:
125
+
126
+ ```json
127
+ {
128
+ "x402Version": 2,
129
+ "resource": { "url": "/api/expensive" },
130
+ "accepts": [
131
+ {
132
+ "scheme": "exact",
133
+ "network": "bsv:mainnet",
134
+ "amount": "100",
135
+ "asset": "BSV",
136
+ "payTo": "1A1zP1...",
137
+ "maxTimeoutSeconds": 60,
138
+ "extra": {
139
+ "partialTx": "<base64 of partial tx template from Gateway.build_template>"
140
+ }
141
+ }
142
+ ]
143
+ }
144
+ ```
145
+
146
+ ### Progressive enhancement via `extra.partialTx`
147
+
148
+ The `extra` field is the Coinbase v2 mechanism for scheme-specific data (EVM uses it for `assetTransferMethod`, `name`, `version`). We use it to carry the partial tx template.
149
+
150
+ **Basic client** (any x402 v2 client): ignores `extra.partialTx`, constructs a tx from scratch using `payTo` + `amount`. This works but has the drawbacks of Profile A — the client must know how to build a BSV transaction from an address.
151
+
152
+ **Smart client** (BSV-aware): reads `extra.partialTx`, deserialises the partial tx, extends it by adding funding inputs and signing. This is Profile B — the gateway has already constructed the payment output. The client just funds it.
153
+
154
+ The template is an optimisation, not a requirement. The `payTo` + `amount` fields are the canonical payment specification and always sufficient on their own. The template is the gateway saying "here, I've already started building this for you."
155
+
156
+ This maps directly to `X402::BSV::Gateway.build_template`:
157
+ - **PayGateway**: template has payment output only → `extra.partialTx` contains a tx with one output
158
+ - **ProofGateway** (if it also contributed to `Payment-Required`): template has nonce input (signed 0xC3) + payment output → `extra.partialTx` contains both
159
+
160
+ BSV appears in the `accepts` array alongside any other chains. A multi-chain server could offer USDC on Base and BSV in the same `Payment-Required` header. The `extra.partialTx` is invisible to non-BSV clients.
161
+
162
+ ### Gateway interface
163
+
164
+ Each gateway implements:
165
+
166
+ ```ruby
167
+ # #challenge_headers(rack_request, route) → Hash
168
+ # Returns header name → value pairs for the 402 response.
169
+ # A gateway may contribute to multiple headers.
170
+ #
171
+ # #proof_header_names → Array<String>
172
+ # The HTTP header names this gateway recognises as proofs.
173
+ #
174
+ # #settle!(header_name, proof_payload, rack_request, route) → result
175
+ # Verify and settle the proof. Raises X402::VerificationError on failure.
176
+ # Returns a result object (with optional receipt headers).
177
+ ```
178
+
179
+ ### Middleware dispatch flow
180
+
181
+ ```ruby
182
+ # Simplified — challenge issuance
183
+ def issue_challenge(rack_request, route)
184
+ headers = {}
185
+ config.gateways.each do |gw|
186
+ gw.challenge_headers(rack_request, route).each do |name, value|
187
+ headers[name] = value
188
+ end
189
+ end
190
+ [402, headers, []]
191
+ end
192
+
193
+ # Simplified — proof dispatch
194
+ def verify_proof(rack_request, route)
195
+ config.gateways.each do |gw|
196
+ gw.proof_header_names.each do |header_name|
197
+ proof = rack_request.get_header(header_name)
198
+ next unless proof
199
+ return gw.settle!(header_name, proof, rack_request, route)
200
+ end
201
+ end
202
+ raise X402::Error, "no recognised proof header"
203
+ end
204
+ ```
205
+
206
+ ### Configuration
207
+
208
+ ```ruby
209
+ X402.configure do |config|
210
+ config.domain = "api.example.com"
211
+ config.payee_locking_script_hex = "76a914...88ac"
212
+
213
+ config.gateways = [
214
+ X402::BSV::ProofGateway.new(
215
+ nonce_key: "...", # or wallet_url: "https://treasury.example.com"
216
+ arc_url: "https://arc.taal.com",
217
+ arc_api_key: "..."
218
+ ),
219
+ X402::BSV::PayGateway.new(
220
+ arc_url: "https://arc.taal.com",
221
+ arc_api_key: "..."
222
+ )
223
+ ]
224
+
225
+ config.protect method: :GET, path: "/api/expensive", amount_sats: 100
226
+ end
227
+ ```
228
+
229
+ ## Two BSV Schemes
230
+
231
+ ### BSV-proof (merkleworks x402 spec)
232
+
233
+ Implements the merkleworks protocol. Client broadcasts, server checks mempool.
234
+
235
+ **Headers**: `X402-Challenge` / `X402-Proof`
236
+
237
+ **Challenge**: merkleworks JSON format including a pre-signed partial tx template (Profile B) with nonce UTXO at input 0 signed with `0xC3`, payment output at output 0, plus request binding metadata (method, path, query, headers hash, body hash), expiry, and `require_mempool_accept: true`.
238
+
239
+ **Client flow**: extend template with funding inputs → delegator adds fees → broadcast → retry with proof (txid + rawtx)
240
+
241
+ **Settlement flow**:
242
+ 1. Decode proof JSON (echoed challenge + payment txid + rawtx)
243
+ 2. Verify echoed challenge (hash, binding, expiry)
244
+ 3. Decode raw tx
245
+ 4. Verify nonce UTXO spent as input at index 0 (challenge binding)
246
+ 5. Verify payment output (amount + payee)
247
+ 6. Check mempool visibility via ARC
248
+ 7. Return result
249
+
250
+ **Requires**: Treasury (for nonce provision + template signing) + ARC (for mempool queries).
251
+
252
+ **Why client broadcasts** (per Rui at merkleworks): Broadcasting is settlement, not authorisation. If the server broadcasts, it takes on transaction submission responsibility, retry/reconciliation logic, and mempool interaction state — pushing it towards a stateful payment processor. Client-side broadcast keeps the server stateless and HTTP authorisation cleanly decoupled from network settlement.
253
+
254
+ ### BSV-pay (our BSV-native scheme)
255
+
256
+ Server broadcasts via ARC. Simpler flow, no nonces needed. Uses Coinbase v2 header spec.
257
+
258
+ **Headers**: `Payment-Required` / `Payment-Signature` / `Payment-Response`
259
+
260
+ **Challenge**: Coinbase v2 `PaymentRequired` structure with `extra.partialTx` carrying the template from `Gateway.build_template` (payment output only, no nonce). See [Multi-protocol Support > Progressive enhancement](#progressive-enhancement-via-extrapartialtx) for the full JSON structure.
261
+
262
+ A basic client constructs a tx from `payTo` + `amount`. A smart client extends `extra.partialTx` by adding funding inputs, signs, and hands the completed tx to the server.
263
+
264
+ **Payment payload** (Coinbase v2 `PaymentPayload` structure with BSV-specific payload):
265
+ ```json
266
+ {
267
+ "x402Version": 2,
268
+ "accepted": {
269
+ "scheme": "exact",
270
+ "network": "bsv:mainnet",
271
+ "amount": "100",
272
+ "asset": "BSV",
273
+ "payTo": "1A1zP1...",
274
+ "maxTimeoutSeconds": 60
275
+ },
276
+ "payload": {
277
+ "rawtx": "<hex-encoded raw transaction>",
278
+ "txid": "<double-SHA256 of raw tx bytes>"
279
+ }
280
+ }
281
+ ```
282
+
283
+ **Settlement flow**:
284
+ 1. Decode `Payment-Signature` header (base64 → PaymentPayload JSON)
285
+ 2. Verify `accepted` matches a valid route
286
+ 3. Decode raw tx from `payload.rawtx`
287
+ 4. Verify payment output (amount + payee)
288
+ 5. Verify OP_RETURN request binding (if present or if strict mode)
289
+ 6. Broadcast to ARC
290
+ 7. ARC 200 → return `Payment-Response` header with settlement result
291
+ 8. ARC error → raise VerificationError (relay ARC response to client)
292
+
293
+ **No nonces needed**: ARC is the replay gate. Each tx can only be accepted once.
294
+
295
+ **Request binding via OP_RETURN**: The partial tx template includes an OP_RETURN output binding the payment to the specific request:
296
+
297
+ ```
298
+ Output 0: payment (amount to payee)
299
+ Output 1: OP_RETURN <SHA256(method + path + query)>
300
+ ```
301
+
302
+ The gateway adds this when building the template. At settlement time, the gateway recomputes the hash from the current request and verifies it matches the OP_RETURN in the submitted tx. This prevents a client from taking a template generated for one URL and submitting it to a different endpoint on the same server.
303
+
304
+ The binding is in the template itself — the client can't modify it because they only append inputs and outputs at higher indices. Cheap (~30 bytes), on-chain, and verifiable.
305
+
306
+ For basic clients that ignore `extra.partialTx` and construct a tx from scratch, binding isn't enforced (they won't include the OP_RETURN). The gateway can choose to require it or not via config — strict mode rejects txs without the binding output, permissive mode accepts either.
307
+
308
+ **Requires**: ARC only. No treasury, no nonce provision.
309
+
310
+ **ARC as the settlement oracle**:
311
+ - ARC 200 OK → relay client request to application
312
+ - Anything else → relay ARC response to client
313
+
314
+ See: https://docs.bsvblockchain.org/important-concepts/details/spv/broadcasting
315
+
316
+ **ARC configuration defaults**:
317
+ - `arc_wait_for: "SEEN_ON_NETWORK"` — tx has propagated to multiple nodes. Safe default for micropayments.
318
+ - `arc_timeout: 5` — seconds. Returns 504 Gateway Timeout if ARC doesn't respond.
319
+ - Both configurable per gateway. Lower-value endpoints could use `ACCEPTED_BY_NETWORK` for speed. Higher-value could bump the timeout.
320
+
321
+ ### Comparison
322
+
323
+ | | BSV-proof (merkleworks) | BSV-pay (ours) |
324
+ |---|---|---|
325
+ | Header spec | Merkleworks `X402-*` | Coinbase v2 `Payment-*` |
326
+ | Challenge header | `X402-Challenge` | `Payment-Required` |
327
+ | Proof header | `X402-Proof` | `Payment-Signature` |
328
+ | Receipt header | — | `Payment-Response` |
329
+ | Template contains | Nonce input (signed 0xC3) + payment output | Payment output + OP_RETURN binding |
330
+ | Who broadcasts | Client | Server (via ARC) |
331
+ | Nonce needed | Yes (challenge binding) | No (ARC is replay gate) |
332
+ | Request binding | Yes (in challenge metadata) | Yes (OP_RETURN in template) |
333
+ | Settlement check | Mempool visibility query | ARC broadcast response |
334
+ | Treasury needed | Yes (nonce provision + signing) | No |
335
+ | Minimum infrastructure | Treasury + ARC | ARC only |
336
+ | Ecosystem compatibility | Merkleworks BSV clients | Any x402 v2 client |
337
+
338
+ ## File Changes
339
+
340
+ ### New files
341
+ - `lib/x402/bsv.rb` — Module entry point
342
+ - `lib/x402/bsv/gateway.rb` — `X402::BSV::Gateway` base class (builds payment-output-only template)
343
+ - `lib/x402/bsv/proof_gateway.rb` — `X402::BSV::ProofGateway` (extends base, adds nonce, merkleworks settlement)
344
+ - `lib/x402/bsv/pay_gateway.rb` — `X402::BSV::PayGateway` (extends base, ARC broadcast settlement)
345
+
346
+ ### Modified files
347
+ - `lib/x402/configuration.rb` — Replace `nonce_provider` + `arc_url`/`arc_api_key` with `gateways` array
348
+ - `lib/x402/middleware.rb` — Multi-gateway dispatch (poll for challenges, route proofs)
349
+ - `lib/x402/protocol/challenge.rb` — Challenge building moves into gateway
350
+ - `lib/x402/verification/pipeline.rb` — Pipeline moves into gateway
351
+ - `lib/x402.rb` — Require `x402/bsv`
352
+ - `DESIGN.md` — Update to reflect dispatcher architecture, template model, two schemes, ecosystem context
353
+
354
+ ### Removed
355
+ - `lib/x402/verification/settlement_checks.rb` — Logic moves into gateway classes
356
+
357
+ ### Specs
358
+ - New specs for `X402::BSV::Gateway`, `X402::BSV::ProofGateway`, `X402::BSV::PayGateway`
359
+ - Update middleware specs for multi-gateway dispatch
360
+ - Update configuration specs
361
+
362
+ ## DESIGN.md Updates
363
+
364
+ 1. **Component boundaries**: Gatekeeper (middleware, no keys), Gateway (holds keys, signs, settles), Treasury (external or local nonce service), Delegator (separate, not our concern).
365
+ 2. **Unified template model**: all gateways produce partial tx templates. ProofGateway adds nonce at index 0 with 0xC3. PayGateway produces payment output only.
366
+ 3. **Middleware as dispatcher**: no blockchain knowledge, polls gateways for challenges, routes proofs.
367
+ 4. **Multi-protocol headers**: payment content negotiation across ecosystems.
368
+ 5. **Two BSV schemes**: BSV-proof (merkleworks) and BSV-pay (ours). Same template model, different settlement.
369
+ 6. **ARC as settlement oracle**: authoritative for BSV-pay.
370
+ 7. **Ecosystem context**: merkleworks BSV spec, Coinbase v2 broad ecosystem, our position.
371
+
372
+ ## Verification
373
+
374
+ 1. `bundle exec rake spec` — all tests pass
375
+ 2. `bundle exec rubocop` — no lint violations
376
+ 3. Boundary test: could someone write `X402::EVM::Gateway` implementing `challenge_headers`, `proof_header_names`, and `settle!` without touching `lib/x402/`? Yes.
377
+
378
+ ## Future Considerations (deferred)
379
+
380
+ - **Coinbase facilitator pattern**: verify-then-settle two-step. Our PayGateway could support this — verify tx structure first, serve resource, broadcast to ARC after.
381
+ - **Extensions mechanism**: gas sponsoring, request binding as extension, custom auth schemes.
382
+ - **Multi-chain accepts array**: BSV alongside EVM chains in the same `Payment-Required` header — now possible since we use the v2 structure.
383
+ - **Runar contract AST**: client sends ContractNode AST, gateway compiles via runar. A future scheme on the same dispatcher.
384
+ - **Header size**: standard BSV payment txs are ~400-600 bytes base64-encoded, well within 8KB+ server limits. Multipart form data for POST requests if this becomes an issue.