Class: X402::BSV::ProofGateway

Inherits:
Gateway
  • Object
show all
Defined in:
lib/x402/bsv/proof_gateway.rb

Overview

Merkleworks x402 compatible gateway.

Challenge: X402-Challenge (merkleworks JSON with nonce UTXO + request binding) Proof: X402-Proof (echoed challenge hash + rawtx + txid)

Supports two modes:

  • Profile A: challenge includes nonce UTXO metadata only
  • Profile B: nonce_provider returns a pre-signed partial_tx template

Constant Summary collapse

ACCEPTABLE_MEMPOOL_STATUSES =

ARC status values that confirm ARC knows about the tx and hasn't rejected it. ARC progresses a broadcast tx through roughly:

RECEIVED → STORED → ANNOUNCED_TO_NETWORK → REQUESTED_BY_NETWORK → SENT_TO_NETWORK → ACCEPTED_BY_NETWORK → SEEN_ON_NETWORK → MINED

Any of these mean "ARC has the tx and is processing it"; the client has demonstrably broadcast. The bad states we must reject are UNKNOWN (never seen), REJECTED, DOUBLE_SPEND_ATTEMPTED — anything in this whitelist is acceptable.

Bitcoin's single-spend guarantee at the network layer is the actual replay gate; this check just confirms the client fulfilled their broadcast obligation. Matches the merkleworks reference implementation's visible check.

%w[
  RECEIVED
  STORED
  ANNOUNCED_TO_NETWORK
  REQUESTED_BY_NETWORK
  SENT_TO_NETWORK
  ACCEPTED_BY_NETWORK
  SEEN_ON_NETWORK
  MINED
].freeze
MEMPOOL_RETRY_DELAYS_SECONDS =

Mempool polling retry schedule. ARC typically returns an intermediate status (RECEIVED, STORED, SENT_TO_NETWORK) for a brief window after broadcast before progressing to SEEN_ON_NETWORK. A short retry loop absorbs that race without adding stateful polling infrastructure.

[0.25, 0.5, 1.0].freeze

Constants inherited from Gateway

Gateway::BRC29_PROTOCOL_ID

Instance Attribute Summary

Attributes inherited from Gateway

#payee_locking_script_hex

Instance Method Summary collapse

Methods inherited from Gateway

#build_template, #request_binding_hash

Constructor Details

#initialize(nonce_provider:, arc_client:, payee_locking_script_hex: nil, wallet: nil, challenge_secret: nil, challenge_store: nil) ⇒ ProofGateway

Returns a new instance of ProofGateway.

Parameters:

  • nonce_provider (#call)

    callable returning nonce UTXO hash; receives (rack_request, payee:, amount:) kwargs. Profile B providers include :partial_tx (binary) in the response.

  • arc_client (#status)

    ARC client for mempool queries

  • payee_locking_script_hex (String, nil) (defaults to: nil)

    payee script (falls back to config)

  • challenge_store (#store!, #lookup, #consume!) (defaults to: nil)

    per-instance challenge cache (default: in-memory, per-process).



64
65
66
67
68
69
70
71
72
# File 'lib/x402/bsv/proof_gateway.rb', line 64

def initialize(nonce_provider:, arc_client:,
               payee_locking_script_hex: nil, wallet: nil, challenge_secret: nil,
               challenge_store: nil)
  super(payee_locking_script_hex: payee_locking_script_hex, wallet: wallet,
        challenge_secret: challenge_secret)
  @nonce_provider = nonce_provider
  @arc_client = arc_client
  @challenge_store = challenge_store || ChallengeStore::Memory.new
end

Instance Method Details

#challenge_headers(rack_request, route) ⇒ Hash

Build a 402 challenge with merkleworks +X402-Challenge+ header.

Parameters:

Returns:

  • (Hash)

    challenge headers

Raises:



80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/x402/bsv/proof_gateway.rb', line 80

def challenge_headers(rack_request, route)
  if route.amount_sats.respond_to?(:call)
    raise ConfigurationError,
          "proof_gateway does not support callable amount_sats (fiat pricing) — use a static value"
  end
  challenge = build_merkleworks_challenge(rack_request, route)
  begin
    @challenge_store.store!(challenge.sha256_hex, challenge)
  rescue ChallengeStore::StoreFullError
    raise VerificationError.new("server at capacity — try again later", status: 503)
  end
  { "X402-Challenge" => challenge.to_header }
end

#proof_header_namesArray<String>

Returns proof header names this gateway responds to.

Returns:

  • (Array<String>)

    proof header names this gateway responds to



95
96
97
# File 'lib/x402/bsv/proof_gateway.rb', line 95

def proof_header_names
  ["X402-Proof"]
end

#settle!(_header_name, proof_payload, rack_request, route) ⇒ SettlementResult

Verify a merkleworks x402 proof against the challenge and check mempool.

Parameters:

  • _header_name (String)

    which proof header matched

  • proof_payload (String)

    base64url-encoded proof

  • rack_request (Rack::Request)
  • route (X402::Configuration::Route)

Returns:

Raises:



107
108
109
110
111
112
113
114
115
116
117
# File 'lib/x402/bsv/proof_gateway.rb', line 107

def settle!(_header_name, proof_payload, rack_request, route)
  required_sats = route.resolve_amount_sats
  proof = Proof.from_header(proof_payload)
  challenge = lookup_challenge!(proof)
  run_protocol_checks!(challenge, proof, rack_request)
  decode_and_verify_transaction!(proof, challenge, required_sats)
  check_mempool!(proof.txid)
  consume_challenge!(proof)

  SettlementResult.new(txid: proof.txid, network: X402.configuration.network)
end