feat(pricing): consume the canonical hub /v1/pricing for dynamic per-capsule $DIG#5
Merged
Merged
Conversation
…capsule $DIG commit/deploy no longer spend a flat 100 DIG. The per-capsule price is dynamic and USD-pegged with ONE canonical source: the hub server computes it (pure formula in dighub_data::pricing + a live DIG->USD oracle) and serves it at GET https://hub.dig.net/v1/pricing as `mint_dig` (base units). digstore consumes that SAME number so it can never diverge from what the hub charges. - digstore-chain: new `pricing` module fetches + parses `mint_dig`, fails LOUD on any unreachable endpoint / bad body / missing-zero price (money-path discipline). - CLI: `explicit_dig_amount` (--dig-amount > DIGSTORE_DIG_AMOUNT > dig.toml) wins and stays deterministic/offline; otherwise the live price is fetched and both DISPLAYED and built into the on-chain DIG-CAT spend. deploy defers the fetch past the --if-changed no-op guard. doctor/setup show the live figure best-effort. - COMMIT_DIG (100_000) kept ONLY as a public symbol sibling repos depend on; it is no longer a spend fallback. - SPEC.md §10 documents the contract; SYSTEM.md (superproject) already maps it. TDD: fetch path, fail-loud path, and explicit-override short-circuit are covered against a local mock endpoint; the offline suite pins DIGSTORE_DIG_AMOUNT. Closes #125
MichaelTaylor3d
added a commit
that referenced
this pull request
Jul 10, 2026
…-up (D-PROOF-PATHLEN)
§9.1 carries an odd node up unchanged; a carried-up leaf skips a level so its
inclusion path is strictly shorter than the full spine. §9.5's ceil(log2 n) is
therefore the worst-case/upper bound on sibling count, attained by the
full-spine leaf (index 0), not a per-leaf equality. ProofStep{hash,is_left} has
no identity encoding under §9.3's fold-and-hash, and forcing == would require
either duplicating odd nodes (changing the root, breaking §9.1 and the D5
cross-crate root agreement) or a non-spec sentinel step. The <= bound is
strictly sound: verify() still recomputes the trusted root (§9.4).
- Record approved deviation in 00-DATASECTION-CONTRACT.md (D8) and design doc (#5)
- merkle.rs module doc states the bound
- Property tests: every leaf path.len() <= ceil(log2 n) AND verify();
full-spine leaf attains the bound; concrete n=3 carry case documented
Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
added a commit
that referenced
this pull request
Jul 10, 2026
…dual #4/#5) Residual #4 — JWT signature verification (guest gate): - verify_request_jwt now fetches the store's trusted JWKS over the session-gated jwks_fetch import (URL from the embedded AuthInfo section, §6.2), parses it, selects the key by kid (or the sole key), and verifies the token signature via jwt::verify_signature (RS256 = rsa PKCS#1 v1.5 over SHA-256, verify-only; ES256 = p256). Claims are still enforced first. - The gate is no longer hardcoded off: GateConfig::from_embedded derives require_jwt from AuthInfo.requires_jwt, wired into get_content/get_proof. - Fails closed -> Decoy on tampered/absent signature, wrong/unknown key, missing JWKS URL, or any claim failure. - Tests: valid-sig+claims -> Real; tampered-sig/wrong-key/unknown-kid/absent- url/expired -> Decoy; existing session-gate + claim tests kept green. Test RSA private key is generated and signs in-test only; guest verifies via (n,e). - Stays no_std (alloc); wasm32-unknown-unknown release build green. Residual #5 — re-evaluate accepted advisories: - deny.toml: removed the stale RUSTSEC-2025-0141 (bincode) ignore — bincode is not in the dependency tree. Kept RUSTSEC-2023-0071 (rsa/Marvin) with a comment: rsa is a direct verify-only dep (JWT RS256), so the decryption-oracle timing attack is N/A; re-evaluate if rsa is ever used to decrypt. - SECURITY.md: residual #4 marked implemented; residual #5 updated (bincode removed, rsa verify-only/Marvin N/A; corrected "transitive" to direct dep). Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
added a commit
that referenced
this pull request
Jul 10, 2026
… (Wave-1 #5/#6/#13/#14) Close the biggest journey gaps before a developer ever spends DIG. Everything in this slice is FREE — no wallet, no chain, no spend — so a developer can scaffold, preview through the real read path, and preview cost before the one paid step. #5 `digstore new <template>` — a free, no-mint scaffolder (today the only on-ramp, `init`, mints 100 DIG into an empty dir). Five templates embedded in the binary (include_str!): static-site, vite-react, next-static, nft-drop, dapp-window-chia. Each writes a runnable local project — a `dig.toml`, a starter app, and (dapp/NFT) a `window.chia` usage example. Refuses a non-empty dir without --force; creates no `.dig` and never touches chain. #6 `digstore dev` — the local inner loop: build-on-save + serve the REAL dig:// read path locally (init_store → add → commit → host runtime → merkle-verify → AES-GCM-decrypt, the same plumbing as compile/cat), with live reload + an injected dev `window.chia` shim. So "renders under dev" == "renders after deploy". Poll-based watcher (no notify dep); multi-thread tokio runtime so a page's asset burst doesn't stall; SPA fallback for routes but a true 404 for missing assets; shims injected only into HTML at request time (never baked into the served bytes). #13 `digstore doctor` + `commit --dry-run` — `doctor` runs pre-publish checks (seed present/unlocked, funds vs 100 DIG + XCH fee, dighub login, default remote reachable, content dir exists) as pass/fail, exits non-zero on a hard fail, never spends. `commit --dry-run` computes + prints the resulting version (root) and the exact DIG/XCH cost with NO seed unlock, NO wallet scan, NO anchor, NO finalize. #14 task-first CLI help — one-line `about` strings lead with the task (commit = "Publish your staged files as a new version"); the commit success line is now a plain "Published a new version — it's live and permanent." with the capsule id below it and the module/coin protocol detail behind --verbose. Shared `dig.toml` reader extracted to `dig_toml::DigToml` (deploy + dev + doctor; removes deploy's private copy). Templates/assets embedded via include_str!. TDD: failing tests first throughout. 178 lib tests + new integration suites (cli_new, cli_dev, cli_doctor, cli_commit_dryrun, cli_help additions) green against the installed binary; cargo fmt + clippy -D warnings clean. Guest wasm built and binary reinstalled per digstore discipline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjQgGAisBupqs7vpkmUhuN
MichaelTaylor3d
added a commit
that referenced
this pull request
Jul 10, 2026
…capsule $DIG (#5) * feat(pricing): consume the canonical hub /v1/pricing for dynamic per-capsule $DIG commit/deploy no longer spend a flat 100 DIG. The per-capsule price is dynamic and USD-pegged with ONE canonical source: the hub server computes it (pure formula in dighub_data::pricing + a live DIG->USD oracle) and serves it at GET https://hub.dig.net/v1/pricing as `mint_dig` (base units). digstore consumes that SAME number so it can never diverge from what the hub charges. - digstore-chain: new `pricing` module fetches + parses `mint_dig`, fails LOUD on any unreachable endpoint / bad body / missing-zero price (money-path discipline). - CLI: `explicit_dig_amount` (--dig-amount > DIGSTORE_DIG_AMOUNT > dig.toml) wins and stays deterministic/offline; otherwise the live price is fetched and both DISPLAYED and built into the on-chain DIG-CAT spend. deploy defers the fetch past the --if-changed no-op guard. doctor/setup show the live figure best-effort. - COMMIT_DIG (100_000) kept ONLY as a public symbol sibling repos depend on; it is no longer a spend fallback. - SPEC.md §10 documents the contract; SYSTEM.md (superproject) already maps it. TDD: fetch path, fail-loud path, and explicit-override short-circuit are covered against a local mock endpoint; the offline suite pins DIGSTORE_DIG_AMOUNT. Closes #125 * build: bump workspace version to 0.8.0 (Cargo.lock) Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
added a commit
that referenced
this pull request
Jul 10, 2026
…-up (D-PROOF-PATHLEN)
§9.1 carries an odd node up unchanged; a carried-up leaf skips a level so its
inclusion path is strictly shorter than the full spine. §9.5's ceil(log2 n) is
therefore the worst-case/upper bound on sibling count, attained by the
full-spine leaf (index 0), not a per-leaf equality. ProofStep{hash,is_left} has
no identity encoding under §9.3's fold-and-hash, and forcing == would require
either duplicating odd nodes (changing the root, breaking §9.1 and the D5
cross-crate root agreement) or a non-spec sentinel step. The <= bound is
strictly sound: verify() still recomputes the trusted root (§9.4).
- Record approved deviation in 00-DATASECTION-CONTRACT.md (D8) and design doc (#5)
- merkle.rs module doc states the bound
- Property tests: every leaf path.len() <= ceil(log2 n) AND verify();
full-spine leaf attains the bound; concrete n=3 carry case documented
Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
added a commit
that referenced
this pull request
Jul 10, 2026
…dual #4/#5) Residual #4 — JWT signature verification (guest gate): - verify_request_jwt now fetches the store's trusted JWKS over the session-gated jwks_fetch import (URL from the embedded AuthInfo section, §6.2), parses it, selects the key by kid (or the sole key), and verifies the token signature via jwt::verify_signature (RS256 = rsa PKCS#1 v1.5 over SHA-256, verify-only; ES256 = p256). Claims are still enforced first. - The gate is no longer hardcoded off: GateConfig::from_embedded derives require_jwt from AuthInfo.requires_jwt, wired into get_content/get_proof. - Fails closed -> Decoy on tampered/absent signature, wrong/unknown key, missing JWKS URL, or any claim failure. - Tests: valid-sig+claims -> Real; tampered-sig/wrong-key/unknown-kid/absent- url/expired -> Decoy; existing session-gate + claim tests kept green. Test RSA private key is generated and signs in-test only; guest verifies via (n,e). - Stays no_std (alloc); wasm32-unknown-unknown release build green. Residual #5 — re-evaluate accepted advisories: - deny.toml: removed the stale RUSTSEC-2025-0141 (bincode) ignore — bincode is not in the dependency tree. Kept RUSTSEC-2023-0071 (rsa/Marvin) with a comment: rsa is a direct verify-only dep (JWT RS256), so the decryption-oracle timing attack is N/A; re-evaluate if rsa is ever used to decrypt. - SECURITY.md: residual #4 marked implemented; residual #5 updated (bincode removed, rsa verify-only/Marvin N/A; corrected "transitive" to direct dep). Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
added a commit
that referenced
this pull request
Jul 10, 2026
… (Wave-1 #5/#6/#13/#14) Close the biggest journey gaps before a developer ever spends DIG. Everything in this slice is FREE — no wallet, no chain, no spend — so a developer can scaffold, preview through the real read path, and preview cost before the one paid step. #5 `digstore new <template>` — a free, no-mint scaffolder (today the only on-ramp, `init`, mints 100 DIG into an empty dir). Five templates embedded in the binary (include_str!): static-site, vite-react, next-static, nft-drop, dapp-window-chia. Each writes a runnable local project — a `dig.toml`, a starter app, and (dapp/NFT) a `window.chia` usage example. Refuses a non-empty dir without --force; creates no `.dig` and never touches chain. #6 `digstore dev` — the local inner loop: build-on-save + serve the REAL dig:// read path locally (init_store → add → commit → host runtime → merkle-verify → AES-GCM-decrypt, the same plumbing as compile/cat), with live reload + an injected dev `window.chia` shim. So "renders under dev" == "renders after deploy". Poll-based watcher (no notify dep); multi-thread tokio runtime so a page's asset burst doesn't stall; SPA fallback for routes but a true 404 for missing assets; shims injected only into HTML at request time (never baked into the served bytes). #13 `digstore doctor` + `commit --dry-run` — `doctor` runs pre-publish checks (seed present/unlocked, funds vs 100 DIG + XCH fee, dighub login, default remote reachable, content dir exists) as pass/fail, exits non-zero on a hard fail, never spends. `commit --dry-run` computes + prints the resulting version (root) and the exact DIG/XCH cost with NO seed unlock, NO wallet scan, NO anchor, NO finalize. #14 task-first CLI help — one-line `about` strings lead with the task (commit = "Publish your staged files as a new version"); the commit success line is now a plain "Published a new version — it's live and permanent." with the capsule id below it and the module/coin protocol detail behind --verbose. Shared `dig.toml` reader extracted to `dig_toml::DigToml` (deploy + dev + doctor; removes deploy's private copy). Templates/assets embedded via include_str!. TDD: failing tests first throughout. 178 lib tests + new integration suites (cli_new, cli_dev, cli_doctor, cli_commit_dryrun, cli_help additions) green against the installed binary; cargo fmt + clippy -D warnings clean. Guest wasm built and binary reinstalled per digstore discipline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjQgGAisBupqs7vpkmUhuN
MichaelTaylor3d
added a commit
that referenced
this pull request
Jul 10, 2026
…capsule $DIG (#5) * feat(pricing): consume the canonical hub /v1/pricing for dynamic per-capsule $DIG commit/deploy no longer spend a flat 100 DIG. The per-capsule price is dynamic and USD-pegged with ONE canonical source: the hub server computes it (pure formula in dighub_data::pricing + a live DIG->USD oracle) and serves it at GET https://hub.dig.net/v1/pricing as `mint_dig` (base units). digstore consumes that SAME number so it can never diverge from what the hub charges. - digstore-chain: new `pricing` module fetches + parses `mint_dig`, fails LOUD on any unreachable endpoint / bad body / missing-zero price (money-path discipline). - CLI: `explicit_dig_amount` (--dig-amount > DIGSTORE_DIG_AMOUNT > dig.toml) wins and stays deterministic/offline; otherwise the live price is fetched and both DISPLAYED and built into the on-chain DIG-CAT spend. deploy defers the fetch past the --if-changed no-op guard. doctor/setup show the live figure best-effort. - COMMIT_DIG (100_000) kept ONLY as a public symbol sibling repos depend on; it is no longer a spend fallback. - SPEC.md §10 documents the contract; SYSTEM.md (superproject) already maps it. TDD: fetch path, fail-loud path, and explicit-override short-circuit are covered against a local mock endpoint; the offline suite pins DIGSTORE_DIG_AMOUNT. Closes #125 * build: bump workspace version to 0.8.0 (Cargo.lock) Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
digstore commit/deployno longer show or spend a flat 100 DIG. They now use the dynamic, USD-pegged per-capsule price from the ONE canonical source — the hub'sGET https://hub.dig.net/v1/pricing(mint_dig, base units) — so digstore can never diverge from what DIGHub charges. Closes #125.What changed
digstore-chain::pricing(new): fetches + parsesmint_digfrom/v1/pricing; fails LOUD on any unreachable endpoint / undecodable body / missing-or-zero price. Pureparse_capsule_priceis unit-tested; conformance test pins the exact amounts ($1 ÷ dig_usd) the hub's owndighub_data::pricingproduces.commit,deploy): an explicit amount (--dig-amount>DIGSTORE_DIG_AMOUNT>dig.tomldig-amount) wins and stays deterministic/offline; otherwise the live price is fetched and both DISPLAYED and built into the on-chain DIG-CAT spend (anchor.update_root(..., dig_amount)).deploydefers the fetch past the--if-changedno-op guard so a no-op deploy never hits the endpoint.doctor/setupshow the live figure best-effort (never fail on a flaky feed).COMMIT_DIG(100_000) kept ONLY as a public symbol sibling repos depend on (dig-nodedig-walletdefault) — no longer a spend fallback.onchain-anchoring.md,dig-cat-payment.md) already documents the dynamic price.Canonical source reused (no second formula)
The price has exactly one computation: the hub server (
packages/dighub-data/src/pricing.rspure formulacapsule_price_baseunits+services/api/src/pricing.rslive DIG→USD oracle: dexieDIG_XCH× CoinGeckochia/usd), served asmint_dig. digstore consumes that number verbatim — it does not reimplement the formula or the oracle. Verified live:GET /v1/pricingreturns HTTP 200 withmint_dig.Blast radius
commit/deploy/doctor/setupcost display + the built spend amount;dig_tomlamount resolution (resolve_dig_amount→explicit_dig_amount). The DIG-CAT payment builder +verify_commit_pays_dig_treasuryare unchanged (they already took the amount as input). No.digformat change.Tests (TDD, money-path)
commit_dry_run_fetches_dynamic_price_not_flat_100,commit_fails_loud_when_price_unavailable_and_no_override,explicit_dig_amount_skips_fetch_even_when_endpoint_down,deploy_dry_run_fetches_dynamic_price, plus updated dry-run cost assertions. Offline suite pinsDIGSTORE_DIG_AMOUNT; pricing tests use a local mock endpoint / an unreachable URL. No test broadcasts a mainnet spend.-D warningsclean ondigstore-chain/digstore-cli.Version
0.7.2 → 0.8.0— minor (new dynamic-pricing capability; backwards-compatible: an explicit amount still works offline).