Skip to content

fix(chain): retry transient coinset failures + resume pending mint (#84) - #3

Merged
MichaelTaylor3d merged 2 commits into
mainfrom
fix/coinset-retry-backoff
Jul 6, 2026
Merged

fix(chain): retry transient coinset failures + resume pending mint (#84)#3
MichaelTaylor3d merged 2 commits into
mainfrom
fix/coinset-retry-backoff

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

TLDR

Fixes DIG-Network/dig_ecosystem#84 — a live user (dkackman) was blocked minting a store on mainnet because coinset.org intermittently truncates an HTTP response body under load (error decoding response body) and digstore's chain-read layer had no retry/backoff/timeout. A single hiccup aborted digstore doctor (fund scan) and digstore init (mint) — once even AFTER the mint spend was already broadcast (submitted mint …), so the flow died with the user's XCH already spent.

What changed

  • Robust coinset RPC wrapper (crates/digstore-chain/src/coinset.rs): every read now goes through Coinset::callretry_core, wrapping the underlying RPC in retry-with-exponential-backoff + full jitter + a per-attempt timeout. Classification: truncated body (is_decode), transport/connect/request/body errors, timeouts, 5xx, 429 are retryable; a definitive not-found / other 4xx is terminal (returns immediately, never a hang). Centralized so every call site benefits — covers get_coin_records_by_puzzle_hashes, _by_hint, _by_puzzle_hash, _by_parent_ids, get_coin_record_by_name, get_puzzle_and_solution, get_blockchain_state, and push_tx.
  • Concurrency: reads are issued sequentially (the wallet scan loops address-by-address), so concurrency is already bounded to 1 — digstore never triggers coinset's parallel-fan-out volume failure (#62). No concurrency limiter added (would be dead code).
  • Post-submit resilience: confirmation polling (ChainAnchor::confirmcoin_record) now survives a transient error via the same retry. Re-running digstore init <name> after a submitted-but-unconfirmed mint RESUMES confirmation of the existing coin (reusing the exact digstore anchor resume logic) instead of minting a second singleton — never double-spends real XCH. A confirmed/non-anchored store on disk is still a genuine name collision (exit 2).
  • No endpoint change — chain reads stay on coinset.org (rpc.dig.net is not a coinset proxy, #62). push_tx retry is safe: the tx id is deterministic, so re-submitting the identical bundle is idempotent at the mempool.

Tests (TDD, all new, kept permanently)

  • retry_recovers_from_transient_then_succeeds — a transient failure then success returns success, not an abort (acceptance a).
  • retry_does_not_retry_terminal_error / coinset_not_found_is_terminal_single_call — a definitive not-found is NOT retried into a hang (acceptance b).
  • retry_exhausts_attempts_on_persistent_transient, retry_recovers_from_per_attempt_timeout — bounded attempts + timeout-is-retried.
  • coinset_retries_truncated_body_then_succeeds — end-to-end through the REAL reqwest path via a local TCP server that truncates the first body then serves valid JSON.
  • confirm_survives_transient_then_confirms — post-submit confirmation survives a transient error → Confirmed (acceptance c).
  • init_rerun_after_pending_mint_resumes_without_reminting — re-running init after a submitted mint resumes; coin_id unchanged proves no re-mint / double-spend (acceptance d).

Verification (this session)

  • cargo test -p digstore-chain: 188 passed, 0 failed (3 ignored live).
  • cargo test -p digstore-cli: 453 passed, 0 failed (2 ignored live) — full integration suite, incl. all init/doctor/anchor tests.
  • cargo fmt --all clean; cargo clippy -p digstore-chain -p digstore-cli --all-targets --all-features -- -D warnings clean.
  • §3.5: cargo install --path crates/digstore-cli --force --lockeddigstore 0.7.1; installed-binary integration tests green.
  • (Local note: cargo clippy --all-targets for the whole workspace hits a pre-existing risc0-zkvm host-linker error in digstore-prover's build script on Windows — unrelated to this change and unaffected by it; CI runs on Linux.)

SemVer

patch 0.7.0 → 0.7.1 — a backwards-compatible bug fix (fix:), no public-API or behavior change beyond added resilience.

coinset.org intermittently truncates an HTTP response body under load
("error decoding response body"); the chain-read layer had no retry/timeout,
so a single hiccup aborted `digstore doctor` (fund scan) and `digstore init`
(mint) — once even AFTER the mint spend was broadcast, leaving the user's XCH
spent while the CLI errored out.

- Wrap every coinset read (Coinset::call -> retry_core) in
  retry-with-exponential-backoff + jitter + a per-attempt timeout.
  Transient failures (truncated body, transport, timeout, 5xx, 429) are
  retried; a definitive not-found / other 4xx is terminal (never a hang).
  Reads are issued sequentially, so concurrency is already bounded to 1.
- Post-submit resilience: confirmation polling now survives a transient error
  (it goes through the same retry). Re-running `init <name>` after a
  submitted-but-unconfirmed mint RESUMES confirmation of the existing coin
  instead of minting a second singleton — it never double-spends real XCH.
- Tests: retry core (recover/terminal/exhaust/timeout), end-to-end
  truncated-body-then-success + not-found-terminal via a local server,
  confirm-survives-transient, and init-rerun-resumes-without-reminting.

No endpoint change — chain reads stay on coinset.org (rpc.dig.net is not a
coinset proxy, #62). Version 0.7.0 -> 0.7.1 (patch: bug fix, no API change).

Closes #84
@MichaelTaylor3d
MichaelTaylor3d merged commit c611248 into main Jul 6, 2026
10 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the fix/coinset-retry-backoff branch July 6, 2026 10:13
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
… deviation #3)

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…viation #3)

Adapts to real digstore_crypto::bls API (bls_sign/bls_verify free fns, to_bytes
returns Bytes48/Bytes96) and digstore_core codec (Encoder/Decoder).

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…dual #3)

SECURITY.md residual #3: the chain source, clock, and proof-backend
selection on the blind serve path are now real and injectable. The default
build stays toolchain-free; real RISC0 proving is "flip the feature +
install the toolchain".

- serve_blind: add BlindServeDeps (Arc<dyn Prover/ChainSource/Clock>) and
  serve_blind_with(). Defaults to the mock/fixed trio (MockProver +
  MockChainSource + FixedClock) so existing serve tests + the default build
  stay green. with_real_chain_clock() swaps in a real ChainSource +
  SystemClock; with_risc0_prover() (cfg(feature="risc0")) builds a real
  Risc0Prover bound to the chain peak. Backend selection compiles in both
  modes: risc0 OFF => MockProver, risc0 ON => Risc0Prover.
- digstore-host: add `risc0` feature forwarding to digstore-prover/risc0
  (NOT in the default build — it pulls risc0-build/embed_methods which needs
  the r0vm toolchain).
- CoinsetChainSource (digstore-prover) is the real coinset.org chain source;
  add HTTP-mocked tests + an #[ignore]d live test (tests/coinset_live.rs)
  and a module-doc note. clock.rs: note SystemClock as the real injectable
  clock.
- SECURITY.md: rewrite residual #3 — chain/clock/backend-selection done;
  real proofs still need the RISC0 toolchain via the risc0 feature.

Default gate green: cargo test --workspace --locked (all pass, live coinset
test ignored), clippy clean, cargo deny advisories/bans/sources ok.

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
The dig-node dig.getContent live read path now carries only REAL trust-bearing
material and never presents a mock/absent proof AS verified:

- inclusion proof — REAL: guest-computed from the module's MerkleNodes
  (build_real_proof), independent of the prover backend (MockProver never
  touches it).
- chain-anchored root — REAL: chain-pinned and fail-closed (#127).
- clock/freshness — INERT on this path: getContent sends window:None with
  require_attestation=false, so the guest temporal gate + the host-attestation
  gate never run; the default FixedClock cannot affect what is served.
- execution proof — ABSENT, never faked: ContentResponse has no execution-proof
  field and dig-node does not implement dig.getProof (returns -32601, not a
  fabricated receipt).

A real risc0 execution attestation is fully wired in code (with_risc0_prover
behind the `risc0` cargo feature) but blocked ONLY on the RISC0 toolchain
(r0vm/rzup) not being installed in CI/this environment — honestly deferred with
that precise blocker (SECURITY.md residual #3), never reported as verified. The
hub/browser shields render that mock/absent state honestly (#134); this keeps
that contract.

Regression tests pin the honest contract:
- get_content_result_carries_real_inclusion_proof_and_no_execution_proof:
  build_result emits the real inclusion proof + the served (chain-pinned) root
  and NO execution_proof/attestation/proof_status/receipt/trusted field.
- get_proof_is_not_served_as_a_verified_proof_by_the_node: dig.getProof returns
  -32601, never a fabricated proof.

SECURITY.md residual #3 extended with the precise dig-node read-path statement.
59 dig-node lib tests green; clippy --all-targets and fmt clean.

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…84) (#3)

* fix(chain): retry transient coinset failures + resume pending mint (#84)

coinset.org intermittently truncates an HTTP response body under load
("error decoding response body"); the chain-read layer had no retry/timeout,
so a single hiccup aborted `digstore doctor` (fund scan) and `digstore init`
(mint) — once even AFTER the mint spend was broadcast, leaving the user's XCH
spent while the CLI errored out.

- Wrap every coinset read (Coinset::call -> retry_core) in
  retry-with-exponential-backoff + jitter + a per-attempt timeout.
  Transient failures (truncated body, transport, timeout, 5xx, 429) are
  retried; a definitive not-found / other 4xx is terminal (never a hang).
  Reads are issued sequentially, so concurrency is already bounded to 1.
- Post-submit resilience: confirmation polling now survives a transient error
  (it goes through the same retry). Re-running `init <name>` after a
  submitted-but-unconfirmed mint RESUMES confirmation of the existing coin
  instead of minting a second singleton — it never double-spends real XCH.
- Tests: retry core (recover/terminal/exhaust/timeout), end-to-end
  truncated-body-then-success + not-found-terminal via a local server,
  confirm-survives-transient, and init-rerun-resumes-without-reminting.

No endpoint change — chain reads stay on coinset.org (rpc.dig.net is not a
coinset proxy, #62). Version 0.7.0 -> 0.7.1 (patch: bug fix, no API change).

Closes #84

* fix(deps): update num-bigint 0.4.7 -> 0.4.8 (0.4.7 yanked; unblocks cargo-deny advisories)

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
… deviation #3)

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…viation #3)

Adapts to real digstore_crypto::bls API (bls_sign/bls_verify free fns, to_bytes
returns Bytes48/Bytes96) and digstore_core codec (Encoder/Decoder).

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…dual #3)

SECURITY.md residual #3: the chain source, clock, and proof-backend
selection on the blind serve path are now real and injectable. The default
build stays toolchain-free; real RISC0 proving is "flip the feature +
install the toolchain".

- serve_blind: add BlindServeDeps (Arc<dyn Prover/ChainSource/Clock>) and
  serve_blind_with(). Defaults to the mock/fixed trio (MockProver +
  MockChainSource + FixedClock) so existing serve tests + the default build
  stay green. with_real_chain_clock() swaps in a real ChainSource +
  SystemClock; with_risc0_prover() (cfg(feature="risc0")) builds a real
  Risc0Prover bound to the chain peak. Backend selection compiles in both
  modes: risc0 OFF => MockProver, risc0 ON => Risc0Prover.
- digstore-host: add `risc0` feature forwarding to digstore-prover/risc0
  (NOT in the default build — it pulls risc0-build/embed_methods which needs
  the r0vm toolchain).
- CoinsetChainSource (digstore-prover) is the real coinset.org chain source;
  add HTTP-mocked tests + an #[ignore]d live test (tests/coinset_live.rs)
  and a module-doc note. clock.rs: note SystemClock as the real injectable
  clock.
- SECURITY.md: rewrite residual #3 — chain/clock/backend-selection done;
  real proofs still need the RISC0 toolchain via the risc0 feature.

Default gate green: cargo test --workspace --locked (all pass, live coinset
test ignored), clippy clean, cargo deny advisories/bans/sources ok.

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
The dig-node dig.getContent live read path now carries only REAL trust-bearing
material and never presents a mock/absent proof AS verified:

- inclusion proof — REAL: guest-computed from the module's MerkleNodes
  (build_real_proof), independent of the prover backend (MockProver never
  touches it).
- chain-anchored root — REAL: chain-pinned and fail-closed (#127).
- clock/freshness — INERT on this path: getContent sends window:None with
  require_attestation=false, so the guest temporal gate + the host-attestation
  gate never run; the default FixedClock cannot affect what is served.
- execution proof — ABSENT, never faked: ContentResponse has no execution-proof
  field and dig-node does not implement dig.getProof (returns -32601, not a
  fabricated receipt).

A real risc0 execution attestation is fully wired in code (with_risc0_prover
behind the `risc0` cargo feature) but blocked ONLY on the RISC0 toolchain
(r0vm/rzup) not being installed in CI/this environment — honestly deferred with
that precise blocker (SECURITY.md residual #3), never reported as verified. The
hub/browser shields render that mock/absent state honestly (#134); this keeps
that contract.

Regression tests pin the honest contract:
- get_content_result_carries_real_inclusion_proof_and_no_execution_proof:
  build_result emits the real inclusion proof + the served (chain-pinned) root
  and NO execution_proof/attestation/proof_status/receipt/trusted field.
- get_proof_is_not_served_as_a_verified_proof_by_the_node: dig.getProof returns
  -32601, never a fabricated proof.

SECURITY.md residual #3 extended with the precise dig-node read-path statement.
59 dig-node lib tests green; clippy --all-targets and fmt clean.

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…84) (#3)

* fix(chain): retry transient coinset failures + resume pending mint (#84)

coinset.org intermittently truncates an HTTP response body under load
("error decoding response body"); the chain-read layer had no retry/timeout,
so a single hiccup aborted `digstore doctor` (fund scan) and `digstore init`
(mint) — once even AFTER the mint spend was broadcast, leaving the user's XCH
spent while the CLI errored out.

- Wrap every coinset read (Coinset::call -> retry_core) in
  retry-with-exponential-backoff + jitter + a per-attempt timeout.
  Transient failures (truncated body, transport, timeout, 5xx, 429) are
  retried; a definitive not-found / other 4xx is terminal (never a hang).
  Reads are issued sequentially, so concurrency is already bounded to 1.
- Post-submit resilience: confirmation polling now survives a transient error
  (it goes through the same retry). Re-running `init <name>` after a
  submitted-but-unconfirmed mint RESUMES confirmation of the existing coin
  instead of minting a second singleton — it never double-spends real XCH.
- Tests: retry core (recover/terminal/exhaust/timeout), end-to-end
  truncated-body-then-success + not-found-terminal via a local server,
  confirm-survives-transient, and init-rerun-resumes-without-reminting.

No endpoint change — chain reads stay on coinset.org (rpc.dig.net is not a
coinset proxy, #62). Version 0.7.0 -> 0.7.1 (patch: bug fix, no API change).

Closes #84

* fix(deps): update num-bigint 0.4.7 -> 0.4.8 (0.4.7 yanked; unblocks cargo-deny advisories)

Co-Authored-By: Claude <noreply@anthropic.com>
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