Skip to content

release: 0.5.0, Rule 28: the explorer calls deleted where the wallet's storage or the header service answers, the fallback shape where a call is irreducible; the toolbox 0.5.0 - #10

Merged
Calgooon merged 12 commits into
mainfrom
rule-28/explorers-release
Oct 9, 2026
Merged

Calgooon merged 12 commits into
mainfrom
rule-28/explorers-release

Conversation

@Calgooon

@Calgooon Calgooon commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

The owner's Rule 28 of 2026-10-09 (bsv-stack-lean NORTH-STAR.md, Rulings), judged in docs/p0/rule-28-explorer-calls.md section 2 and executed by docs/p0/rule-28-cli.md, nine items each with a witness red then green: export-beefs builds each BEEF from the wallet's own storage (C10); the spender's confirmation is a proof, never an explorer's count (C8); the tip height and the median time past from the header service (C13); the output index from the BEEF's own transaction (C3); one spend probe with three answers and the toolbox's second provider (C5, C7); a second chain index beside WhatsOnChain with a rotating start (C1); the gift commands through the wallet's services and broadcasters (C11, C12); sync named as a chain scan and a break-glass read at the site and in --help (C4); the courier named with a second provider (C2). The toolbox at 0.5.0 from crates.io. No limit changed. Verified by the captain at the tip against the registry: 310 tests pass; clippy and fmt clean; the publish dry run packages 117 files. Four open questions for the owner are in the report.

🤖 Generated with Claude Code

Calgooon and others added 12 commits October 9, 2026 12:36
…r without one

The toolbox 0.5.0 takes the tip height from the header service and asks no
explorer (its Rule 28 item 1). test_get_height asked a live explorer through
the default Services; it now stands a local header service in front of the
wallet and expects that service's tip, and a second test expects no height
when no header service is configured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… own storage

export-beefs asked WhatsOnChain for /tx/{txid}/beef once per transaction
holding an unspent output: one explorer request per output, for proofs the
wallet already holds. It now reads the outputs with their transactions
(list_outputs, include: entire transactions), which is the same walk over
the stored transactions and proofs that builds an outgoing BEEF, and cuts
one BEEF per txid from it. A transaction storage cannot carry back to a
proof is counted as failed and named; nothing is fetched from a third
party. The command holds no HTTP client.

Witness: export_beefs_writes_each_beef_from_the_wallets_own_storage. Red
at the base's loop with a local fixture in the explorer's place (one
request to /tx/{txid}/beef, nothing written while the fixture answered
500); green with no explorer in the code path.
export_beefs_names_no_explorer_and_no_http_client reads the source.

src/test_support.rs: a local fixture server that counts requests, and a
hand-built transaction, for this and the following items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…explorer's count

probe_input_spend read WhatsOnChain's /tx/hash/{spender} and took its
confirmations field as 'the spend is mined'. That field decides
RELINQUISHED (cleanup-abandoned, reconcile-outputs, the daemon's sweep),
and an explorer's count is a word. The probe now asks the wallet's services
for the spender's merkle path: Services::get_merkle_path returns a path only
after its root matched the header service's header, and none when no header
service is configured. No proof (not mined yet, a courier fault, no header
service) is confirmed: false, and the coin stays as it is. The explorer
request is gone.

reconcile() takes the services; the daemon's sweep and the two commands
pass the wallet's own.

Witness: the_spenders_confirmation_is_a_proof_never_an_explorers_count. Red
at the base: confirmed: true on a fixture explorer's {"confirmations":5}
with no proof to be had. Green: false without a proof, true with one, zero
requests to /tx/hash/{spender}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n time past from the header service

gift-inspect asked WhatsOnChain's /chain/info for blocks and mediantime.
Both are header facts the header service already answers: the tip header's
height, and the median of the times of the tip and its ten predecessors
(the node's rule, cited from the sibling: bsv-script-lean
docs/BLOCK-CONSENSUS.md section 2.2). chain_clock reads the tip header and
the ten headers before it through the wallet's services and takes the
median as the node does. With no header service the command stops with an
error naming CHAINTRACKS_URL; no explorer's number stands in.

Witness: the_tip_and_the_median_time_past_come_from_the_header_service.
Red at the base's request with a local fixture in the explorer's place
(one request to /chain/info, its numbers taken as the clock). Green: the
height and the median from eleven headers, out of order; the source names
no /chain/info.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wn transaction

receive with no --vout asked WhatsOnChain's /tx/{txid} for a decoded copy
of the outputs and matched our address in it, then fetched the BEEF of the
same transaction. The BEEF carries the transaction's own bytes, so the
BEEF is fetched once and the output paying our deposit script is found in
it. The second explorer request and the three structs that decoded it are
gone. A transaction that pays us nothing is an error, as before.

brc29::deposit_script: the P2PKH script of the deposit address, tied to
the address by a test.

Witness: the_output_index_is_read_from_the_beefs_own_transaction. Red at
the base's two requests with a local fixture in the explorer's place (a
request to /tx/{txid}, and no answer when it failed). Green: one request,
the BEEF, and the index from its transaction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in sync.rs deleted, two explorers behind 'unspent'

An output's spend was asked two ways with two error rules. sync
--reconcile-spent held its own copy: any failure of WhatsOnChain's spent
route read as 'not spent', any 200 relinquished the output whether or not
the spender was mined, and a second request asked whether the parent
existed. probe_input_spend had the three answers but one explorer, and its
'unspent' was that one explorer's 'no spend known' plus the parent's
presence.

There is now one function, and sync asks it. Its shape:

1. Who spent it: WhatsOnChain's /tx/{txid}/{vout}/spent, the one explorer
   route we hold that names a spender. A name is checked by a proof (item
   2). A 404 or a fault is not an answer and falls through.
2. Is it unspent: the toolbox's is_utxo (bsv-wallet-toolbox-rs 0.5.0, its
   Rule 28 item 5): WhatsOnChain and Bitails, a rotating start, a positive
   from the one that gives it, a negative only from both. Unspent is the
   outpoint in an unspent set. Two explorers agreeing it is not unspent
   while no spender is named (a phantom parent reads this way) is Unknown.

The parent-existence request (C6 in sync.rs, C9 in the probe) is gone with
the rule it served: an outpoint in an unspent set has a parent. The script
the unspent set is asked with comes from our own rows
(stored_locking_script).

sync --reconcile-spent relinquishes only on a proven spend. Its report
changes: 'phantom_parent' is replaced by 'spent_unproven' and 'unknown'
(JSON and text).

Witnesses, red then green: one_explorers_no_spend_known_is_not_unspent
(red: Unspent on one explorer's 404 and the parent's 200),
a_fault_at_the_spender_lookup_falls_through_to_the_unspent_set (red:
Unknown on the first fault), sync_holds_no_copy_of_the_spend_probe (red:
both request lines in sync.rs), only_a_proven_spend_relinquishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ting start, absence only from both

'Has the network seen a transaction we just sent' was asked of the
broadcaster and of one chain index, WhatsOnChain, always in the same
place. The verifier now holds two chain indexes, WhatsOnChain and Bitails
(GET /tx/{txid}, the route the toolbox's own status read uses), and asks
them as one question with one answer (ask_chain_indexes):

- the start rotates, once per probe round, shared by every clone;
- a positive from the index that gives it ends the question;
- absence is every index answering 404; an index that could not look
  leaves the answer Unknown, whatever the other said.

The absence rule stays: Rejected is still the broadcaster's absence AND the
chain indexes' absence in the last round of the window, and the reconciler's
absence clock still runs on the same report. What changes is that one
explorer's 404 is no longer the chain's absence. No attempt count, delay,
timeout or probe bound moved; a transaction absent from both indexes costs
two index requests a round where it cost one.

Witnesses, red then green:
one_chain_indexs_absence_is_not_absence_while_the_other_could_not_look
(red: Rejected on one explorer's 404), the_chain_index_start_rotates (red:
4 requests to the first and 0 to the second), bitails_is_a_second_chain_index
(red: WhatsOnChain alone). Controls: both absent is Rejected; the second
index's positive stands after a negative or a fault.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…services; gift-claim broadcasts through the wallet's broadcasters

gift-claim and gift-inspect each held a private copy of the WhatsOnChain
base and an HTTP client, and fetched the deposit's bytes from
/tx/{txid}/hex themselves: one explorer, no check that the bytes were the
transaction asked for. gift-claim then posted the claim to WhatsOnChain's
/tx/raw, past the wallet's broadcasters and their memory.

commands/gift_chain.rs holds the two things the gift commands ask of the
chain:

- deposit_bytes: Services::get_raw_tx. The toolbox hashes the bytes to the
  txid, a second explorer stands behind the first, and 'no explorer has
  it' is an error apart from 'could not look'.
- broadcast_claim: Services::post_beef, the ladder the wallet's own sends
  use, with a BEEF of the deposit (with its proof when the header service
  checks one) and the claim. The provider that accepted is reported;
  gift-claim's JSON 'broadcast' field names it (it was the constant
  'whatsonchain').

Neither command names an explorer or holds an HTTP client.

Witness: the_gift_commands_hold_no_explorer_and_no_http_client. Red at the
base (gift_claim.rs names reqwest). Green, with
the_deposits_bytes_come_through_the_wallets_services,
the_claim_is_broadcast_through_the_wallets_broadcasters,
a_mined_deposit_carries_its_checked_proof and
a_refused_claim_is_an_error_naming_the_refusal against the toolbox's mock
services.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead, at the site and in --help

sync asks an explorer which outputs pay our deposit address: an address
scan, the one question that has no header, proof or own-index answer by
construction. Its help read 'Scan WhatsOnChain for unspent UTXOs at our
deposit address and internalize new ones', as if it were the way to
receive.

- sync --help now opens 'Break-glass chain scan', says what it is kept for
  (finding a payment nobody handed us), that no daemon path runs it, that
  the routine path is fund with the BEEF the payer hands over, and that an
  empty list is not 'nothing was paid'. fund's help says it is the routine
  way to receive.
- the request line carries the reason in the rule's words, and the command
  logs a warn line (marker break_glass_chain_scan) each time it runs.

No behaviour of the scan changed: one explorer, an HTTP fault is an error,
an empty list removes nothing.

Witnesses, red then green:
sync_help_says_it_is_a_break_glass_chain_scan_and_names_fund (red: the
help lacked every one of the four words), the_address_scan_is_named_at_the_site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a second courier behind it

receive fetches a transaction's BEEF from WhatsOnChain's /tx/{txid}/beef:
one provider, and its fault was the command's error. The call is
irreducible (a payment to a bare address arrives with no BEEF from its
sender) and its answer is self-verifying (the BEEF is internalized: its
transaction hashes to the txid, its merkle paths meet the header service's
roots), so the explorer is a courier and is now named one at the site and
in receive --help.

Behind it stands a second courier, the toolbox's get_beef (the bytes from
either explorer, a merkle path only after its root met the header
service). WhatsOnChain stays first because its BEEF alone carries an
unmined transaction's ancestors. A fault at the first falls through (a
warn line, marker break_glass_beef_courier); when neither delivers the
error is 'could not look' with what each said.

Witness: a_courier_fault_falls_through_to_the_second_courier. Red at the
base (the fixture's 500 was the result). Green, with
the_second_courier_is_not_asked_while_the_first_delivers and
no_courier_delivering_is_could_not_look_with_both_reasons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st holds the list

Five explorer request lines remain in the CLI, each an irreducible
break-glass read: the two chain indexes of the broadcast verifier (C1:
WhatsOnChain /tx/hash/{txid}, Bitails /tx/{txid}), receive's BEEF courier
(C2: /tx/{txid}/beef), sync's address scan (C4: /address/{addr}/unspent)
and the spend probe's spender lookup (C7: /tx/{txid}/{vout}/spent). Each
now carries, directly above the request line, the reason no header, proof
or own-index answer exists, tagged 'Break-glass (Rule 28, C<n>)'.

tests/rule_28_sites.rs reads the source: every listed request line has its
reason within twelve lines above it; no file outside the list names an
explorer host; no unlisted file holds an HTTP client; no unlisted request
line exists. Red before the comments (the Bitails, courier and spender
lines had no tag at the site); green after. Comments and one reordered
log line only; no behaviour changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rule 28: an explorer is asked only where no header, no merkle proof and no
row of the wallet's own answers the question, and then in one shape: two
providers where a second exists, a rotating start, a negative only when
both agree, and "could not look" kept apart from "nothing there". The CLI
held 13 explorer request lines at 0.4.0; it holds 5, each naming its
reason at the site (tests/rule_28_sites.rs holds the list).

Needs bsv-wallet-toolbox-rs 0.5.0.

Deleted (a header, a proof or our own storage already had the answer):

1. export-beefs builds each BEEF from the wallet's own storage; it asked
   an explorer once per unspent output.
2. A spender's confirmation is a merkle proof whose root the header service
   checked, never an explorer's confirmations count (cleanup-abandoned,
   reconcile-outputs, the daemon's sweep).
3. gift-inspect takes the tip height and the median time past from the
   header service.
4. receive reads the output index from the BEEF's own transaction.

Kept, in the fallback shape:

5. One spend probe (three answers) for cleanup-abandoned,
   reconcile-outputs, the daemon's sweep and sync --reconcile-spent; the
   copy in sync is gone. "Unspent" is the toolbox's two-explorer read
   (WhatsOnChain and Bitails); one explorer's "no spend known" is not it.
6. The broadcast verifier asks two chain indexes, WhatsOnChain and Bitails,
   with a rotating start: present from either, absent only from both. The
   absence rule (the broadcaster AND the chain index, after the window) is
   unchanged.
7. gift-inspect and gift-claim read the deposit through
   Services::get_raw_tx; gift-claim broadcasts through the wallet's
   broadcasters.
8. sync says, at the site and in --help, that it is a chain scan and a
   break-glass read; the routine way to receive is fund with a BEEF from
   the payer.
9. receive's BEEF fetch is named a courier and has the toolbox's get_beef
   behind it.

What an operator sees change:

- CHAINTRACKS_URL was already required. With it set to off, getHeight is
  an error (it was an explorer's number), gift-inspect stops, and no spend
  is ever proven, so nothing is relinquished.
- sync --reconcile-spent relinquishes only an output whose spender we hold
  a merkle proof of. Its report replaces "phantom_parent" with
  "spent_unproven" and "unknown" (JSON and text).
- An input is restored as unspent only when an explorer lists it in an
  unspent set; a spent route's 404 alone no longer does it. With both
  explorers unreadable the input stays locked and is asked again.
- A transaction is declared absent only when WhatsOnChain and Bitails both
  answer 404 (with the broadcaster's own absence). With one of them
  unreachable nothing is retired; it is kept and asked again.
- gift-claim's JSON "broadcast" field names the broadcaster that accepted
  the claim (it was the constant "whatsonchain").
- export-beefs writes the BEEF storage builds (BEEF V2 hex); a transaction
  storage cannot carry back to a proof is counted as failed, not fetched.

No limit changed: no attempt count, delay, timeout, page size or probe
bound was added or moved. No stored data changes and there is no
migration; 0.4.1 reads everything 0.5.0 writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Calgooon
Calgooon merged commit 9235de5 into main Oct 9, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant