Skip to content

chore(deps): bring digstore-chain onto the chia-wallet-sdk 0.36 line - #59

Merged
MichaelTaylor3d merged 1 commit into
mainfrom
loop/3161-digstore-chain-036
Aug 27, 2026
Merged

chore(deps): bring digstore-chain onto the chia-wallet-sdk 0.36 line#59
MichaelTaylor3d merged 1 commit into
mainfrom
loop/3161-digstore-chain-036

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

DO NOT MERGE — draft, handed back for the gate round. Not undrafted deliberately (§2.4a).

Parent epic: https://github.com/DIG-Network/dig_ecosystem/issues/3161

Closes digstore-chain's four held chia-* declarations, the last in-scope slice of #3161.
A previous lane made this change, measured it, and correctly reverted rather than shimming,
because datalayer-driver 4.0.0 hard-required chia-wallet-sdk ^0.34.0.
datalayer-driver 5.0.0 (published 2026-08-26, built against chia-wallet-sdk 0.36.0)
is what unblocks it.

What moved

crate from to
datalayer-driver 4 5
chia-sdk-coinset 0.34 0.36
chia-sdk-test 0.34 0.36
chia-sdk-types 0.34 0.36
chia-wallet-sdk 0.34 0.36
dig-wallet-backend 0.29 0.31
dig-constants 0.10 0.11
dig-store (digstore-cli) 0.7 0.8

The last three are not scope creep — they are what makes the line single. dig-wallet-backend
0.29 and dig-constants 0.10 were each held by a documented condition that no longer holds
(dig-tips 0.2 pinned dig-constants ^0.10; dig-tips 0.3 declares ^0.11), and
dig-store 0.7 declares chia-wallet-sdk ^0.34, so leaving it would have kept the SDK
resolving twice inside one workspace. cargo tree -i chia-wallet-sdk@0.34.0 now returns
nothing. Both stale "held deliberately" comment blocks are removed rather than reworded.

Remaining cargo tree -d duplicates (chia-bls 0.28.2/0.42.1, chia-sha2, chia-traits)
enter through clvmr / chialisp / the rue-* toolchain, not through any declaration here.

Blast radius checked

gitnexus was not used: this is the primary shared checkout, not a lane worktree, and
§2.0 forbids indexing the superproject. Fallback per §2.0 bound (2) — ripgrep across all of
modules/ plus direct reads of every consumer manifest.

  • The renamed types are named by nobody outside this crate. digstore_chain::…DataStore
    has zero hits ecosystem-wide. dig-node-core (lib.rs:40,
    seams/chia_peer/coinset_resolver.rs:9) imports sync_datastore /
    sync_datastore_with_history / verify_pinned_root and binds the result without naming
    its type.
  • Every external consumer pins by git rev, so none of this reaches them until they choose
    to bump: hub.dig.net anchor-watcher (rev 099f88ae), hub services/api, on.dig.net,
    dig-node dig-wallet (rev 4c34f0be). Their manifests already carry notes naming this
    crate as the thing holding them on 0.34 — this PR is what unblocks those bumps, which are
    separate units of work in their own repos.
  • vc had no callers anywherepub mod vc; in lib.rs was its only reference in any
    repo, and it appears in no SPEC.md and no docs page.
  • In-workspace: digstore-cli and dig-resolver consume digstore-chain; both compile
    unchanged. cargo check --workspace --all-targets --locked is clean.

detect_changes() was likewise unavailable (no per-worktree index); the diff was verified by
git diff --stat — 8 files, confined to two manifests, the root manifest, the lock, and three
digstore-chain sources.

Decision: propagate the rename, do not alias

DataStoreDatastore, DataStoreMetadataDatastoreMetadata, DataStoreInfo
DatastoreInfo. DataStoreInnerSpend is unchanged — it is datalayer-driver's own type.

Aliasing was considered and rejected on measurement, not preference. chip35_dl_coin aliases
because its names are published TypeScript that consumers really do type; that reason does not
apply here — the only consumers are Rust, they pin by git rev, and not one of them names the
type
. An alias would therefore buy zero compatibility while leaving this crate speaking a
spelling upstream has abandoned.

Decision: withdraw vc, do not port or vendor it

chia-sdk-driver 0.36 deletes primitives/action_layer/verification.rs,
verification_asserter.rs and verification_info.rs while the rest of the action layer
(catalog_registry, reward_distributor, xchandles_registry, …) survives — a deliberate
upstream removal, with no replacement anywhere in chia-wallet-sdk 0.36. Re-deriving those
CLVM puzzles inside digstore-chain would be a rival implementation of removed upstream code,
and unreviewed puzzle code on an on-chain attestation path at that. The surface is withdrawn
rather than faked, with the reason recorded at its former site in lib.rs.

This is a capability removal and the reviewer should weigh it as one. It is not required
to make the bump compile in the narrow sense — vendoring would also compile. It is judged the
honest option because the on-chain primitive the module attested against is no longer shipped.

Verification

Load-bearing test, both sides pinned. Datastore::from_spend — the forward lineage walk,
verify_pinned_root, sync_datastore — parses spends authored by whoever spent the parent
coin, and reaches DelegatedPuzzle::from_memos (datastore.rs:203) and run_metadata_updater
(datastore.rs:349). The oracle fee is the only attacker-controllable value in that parse that
becomes a number, so it is the only one a coercion can turn into a plausible wrong answer.
singleton::oracle_fee_memo adds five tests: a control (an honest 1000-mojo fee parses to
exactly 1000, so the hostile cases cannot pass against a parser that simply refuses everything)
plus the four hostile encodings.

The over-u64 fixture is 2^64 + 1000, chosen so its low 64-bit digit is the control's
believable 1000. That is the point: a truncating parser does not fail loudly there — it
reports an ordinary fee nobody encoded.

Revert-proof. Rather than reverting the shared checkout (destructive with sibling lanes
live), the identical five fixtures were run against chia-sdk-driver =0.34.0 in an isolated
scratch crate:

a_fee_above_u64_is_refused_not_truncated_to_its_low_digit  FAILED
    an over-u64 oracle fee was coerced to 1000 instead of being refused
a_negative_fee_is_refused_not_read_as_its_magnitude        FAILED
    a negative oracle fee was accepted as 1
an_empty_fee_memo_is_zero_not_a_panic                      FAILED
    index out of bounds: the len is 0 but the index is 0  (datastore_info.rs:81)
an_explicit_zero_fee_memo_is_zero_not_a_panic              FAILED
    index out of bounds: the len is 0 but the index is 0  (datastore_info.rs:81)
an_in_range_fee_parses_to_exactly_itself                   ok
test result: FAILED. 1 passed; 4 failed

Four fail on 0.34, the control passes on both — which is exactly the shape a non-vacuous
proof should have.

Golden vectors reproduce unmodified. No fixture file was edited. digstore-remote's
golden_read_proof suite passes untouched.

One test fixture changed, and it is a third upstream behaviour change.
streaming::tests::create_then_clawback_remainder timestamped its clawback block at exactly
the ASSERT_BEFORE_SECONDS_ABSOLUTE bound. chia-sdk-test 0.34 rejected only
bound < timestamp (simulator.rs:240); 0.36 rejects bound <= timestamp
(spend_bundle_validation.rs:178), which is the mainnet rule. The simulator was one second
too permissive and this test was passing on that bug
— mainnet would have rejected the same
spend. Product code is unchanged; the block is now timestamped one second earlier.

Results.

gate result
cargo fmt --all --check exit 0
cargo clippy --workspace --all-targets --locked -- -D warnings … (as ci.yml) exit 0, unpiped
cargo test --workspace --locked 39 binaries, 0 failures (252 in digstore-chain)
scripts/local-push-test.sh (e2e §21 push/pull/clone) 5/5 pass
cargo build --workspace --locked clean

Installed binary (§3.5). Guest wasm built first, then
cargo install --path crates/digstore-cli --force --locked, then a real mainnet read through
the uplifted stack — coinset client 0.36 plus the 0.36 Datastore::from_spend lineage walk:

$ digs store-status 8c4b47f6d685e170ea663656d5cd2bdc8a1880efe5af285975e185974a7eded5 --json
{ "status": "live", "confirmations": { "have": 165689, "target": 32 },
  "live_root": "ac876a9f…0222c7", "verified": true, "generation_count": 2 }

digs init was not run — it mints on mainnet and nothing here needs it.

Version

0.28.10.29.0. Minor under 0.x SemVer: the public surface changes shape (type
rename + module withdrawal), which is breaking for a 0.x crate and therefore a minor bump.

Found, not fixed

  1. Four downstream consumers are still on 0.34 via git-rev pins and each carries a comment
    naming digstore-chain as its blocker. This PR unblocks them; the bumps belong in
    hub.dig.net, on.dig.net and dig-node as their own units of work.
  2. dig-wallet and dig-node-core pin digstore-chain by git = … rev, which is an NC-7
    violation (all crates publish to crates.io; consumers depend by version) independent of this
    uplift. digstore-chain is published, so those pins can become version deps.
  3. The vc capability is gone with no replacement upstream. If DIG needs on-chain
    attestations, that is now a design question rather than a dependency question.

`digstore-chain` held `chia-sdk-coinset`, `chia-sdk-test`, `chia-sdk-types` and
`chia-wallet-sdk` at 0.34 while its own primitives were already on 0.36.1, because
`datalayer-driver` 4.0.0 hard-required `chia-wallet-sdk ^0.34.0` and this crate passes
DataLayer singleton types straight across that boundary. `datalayer-driver` 5.0.0 is
built against `chia-wallet-sdk` 0.36.0, so the whole family can move as one set.

Moved together, so the workspace resolves each family exactly once:

  datalayer-driver     4      -> 5
  chia-sdk-coinset     0.34   -> 0.36
  chia-sdk-test        0.34   -> 0.36
  chia-sdk-types       0.34   -> 0.36
  chia-wallet-sdk      0.34   -> 0.36
  dig-wallet-backend   0.29   -> 0.31
  dig-constants        0.10   -> 0.11
  dig-store            0.7    -> 0.8   (digstore-cli; 0.7 kept the SDK resolving twice)

`cargo tree -i chia-wallet-sdk@0.34.0` now finds nothing, and the two documented
"held deliberately" blocks are removed rather than reworded: the conditions they
described (dig-tips 0.2 pinning dig-constants ^0.10; the driver on ^0.34) no longer hold.

API adaptation
--------------
`chia-wallet-sdk` 0.36 respells the DataLayer types, so the re-exports and every use
site take the upstream spelling: `DataStore` -> `Datastore`, `DataStoreMetadata` ->
`DatastoreMetadata`, `DataStoreInfo` -> `DatastoreInfo`. `DataStoreInnerSpend` keeps its
name (it is datalayer-driver's own type, not the SDK's).

The old spellings are NOT kept behind an alias. No consumer anywhere in the ecosystem
names the type — `dig-node-core` and `dig-wallet` call `sync_datastore` and bind the
result without naming it, and hub.dig.net / on.dig.net pin this crate by git rev — so an
alias would buy no compatibility and would leave the crate speaking a name upstream no
longer uses.

The `vc` module is WITHDRAWN, not ported. It wrapped the SDK verification layer
(`Verification` / `VerifiedData` / `VerificationAsserter`), which upstream removed in
0.36: `chia-sdk-driver`'s `primitives/action_layer/verification*.rs` are gone while the
rest of the action layer survives, so the removal is deliberate and the on-chain
primitive the module attested against no longer ships. Re-deriving those puzzles here
would be a rival implementation of removed upstream CLVM. The module had no callers in
any repo and appears in no SPEC or docs page.

Behaviour riding along
----------------------
This crate is on both changed upstream paths, transitively and unavoidably:
`Datastore::from_spend` — called by the forward lineage walk, `verify_pinned_root` and
`sync_datastore`, on spends authored by whoever spent the parent coin — reaches
`DelegatedPuzzle::from_memos` and `run_metadata_updater` inside the driver.

`from_memos` is the one attacker-controllable value in that parse that becomes a NUMBER,
and 0.34 coerced it four ways: an empty memo panicked, an encoded zero panicked, a
negative fee was read as its positive magnitude, and a fee above `u64` was truncated to
its low 64-bit digit. 0.36 returns `0` or `DriverError::InvalidMemo`. A new conformance
test pins the refusal from both sides — a control asserting an honest 1000-mojo fee
still parses to exactly 1000, and four hostile encodings. The over-`u64` fixture is
`2^64 + 1000` specifically because its low digit is the control's believable `1000`: a
truncating parser does not fail loudly there, it reports an ordinary fee nobody encoded.

Test fixture
------------
`streaming::tests::create_then_clawback_remainder` timestamped its clawback block at
exactly the `ASSERT_BEFORE_SECONDS_ABSOLUTE` bound. chia-sdk-test 0.34 rejected only
`bound < timestamp`, letting the equal case through; 0.36 rejects `bound <= timestamp`,
which is the mainnet rule. The block is now timestamped one second earlier. No product
code changed — the old fixture depended on a simulator that was one second too permissive.

Refs: DIG-Network/dig_ecosystem#3161

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d force-pushed the loop/3161-digstore-chain-036 branch from 41181f6 to 4bef5f5 Compare August 26, 2026 23:39
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

loop-security audit — IN PROGRESS

Auditing head 4bef5f5b49d4402977e1f55334bc3de02f74268e (full tier). Findings will be posted to this
thread as they are formed; a final verdict comment follows. Do not undraft until the verdict lands.

Scope of this leg: vc.rs withdrawal (capability removal), the simulator-rule fixture edit, the
rename propagation, the 8 dependency moves, and the Datastore::from_spend memo-parsing paths.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Security audit — interim finding 1 of 5: the vc.rs withdrawal is CORRECT

Head audited: 4bef5f5b49d4402977e1f55334bc3de02f74268e.

I verified the "zero callers" claim independently rather than taking it on trust, because it is the
single load-bearing fact that makes a capability removal safe.

Reachability — nothing reaches it.

  • Not re-exported. crates/digstore-chain/src/lib.rs:28 at the merge-base carried exactly one
    reference, pub mod vc;, and there is no pub use vc::… anywhere in the crate. So the module
    was reachable only by its own path, never laundered through a facade re-export.
  • No in-repo caller. At head, zero matches in crates/ for crate::vc, chain::vc, mod vc, or
    any of the nine public symbols (build_vc_issue, build_vc_revoke, vc_asserter,
    vc_asserter_puzzle_hash, credential_id, vc_transfer_unsupported, sign_vc_spends,
    vc_launcher_coin, MintedCredential/CredentialData).
  • No feature gate. The module was unconditional, so there is no --features combination that
    reaches it and no cfg-hidden path a lexical grep would miss.
  • Org-wide, not just locally. GitHub code search across org:DIG-Network returns
    total_count = 1 for each of vc_asserter, build_vc_issue, MintedCredential — and in every
    case the single hit is DIG-Network/digs crates/digstore-chain/src/vc.rs, i.e. the file being
    deleted. chain::vc and digstore_chain::vc return 0. This covers consumer repos whose local
    checkout may lag, which a working-tree grep does not.
  • No wasm/JS surface. crates/dig-client-wasm is excluded from the workspace and does not name
    any vc symbol; the crate exposes no binding that could surface it to a JS consumer.

No stranded data. The module built spends against upstream's verification-layer puzzles; it
persisted no format of its own, and no golden fixture, puzzle hash, or on-disk artifact in this repo
references it. Nothing already on chain becomes unreadable — the crate never wrote a .dig section,
a store-format field, or a persisted record on this path, so §5.1 back-compat is not engaged.

Judgement, stated plainly: withdrawal is right and vendoring would have been the worse choice.
Upstream deleted primitives/action_layer/verification*.rs while keeping the rest of the action
layer — a targeted removal, not collateral damage from a refactor. Vendoring those puzzles here would
have produced a second, unmaintained implementation of attestation CLVM on an on-chain path, owned by
a crate that is not the canonical home for it, with no upstream to track for a fix. That is precisely
the rival-implementation / byte-drift class the contract forbids, and on an attestation path the
failure mode is a credential that verifies here and nowhere else. A dead-code module is a smaller
liability than a live rival one.

Not gating. One residual note for the epic rather than this PR: the removal is a real (if unused)
capability reduction, so if verifiable credentials are ever wanted again the right move is an
upstream-supported primitive, not a resurrection of this file from git history.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Security audit — interim findings 2-4 (head 4bef5f5b49d4402977e1f55334bc3de02f74268e)

2. The simulator diagnosis is CORRECT, and in the right direction. Genuine find.

I read both conditions in the published crates rather than trusting the summary, then checked the
mainnet rule against a third source.

  • chia-sdk-test 0.34.0, src/simulator.rs:239-240seconds < self.data.next_timestamp
    → rejects only strictly less, so seconds == next_timestamp was ACCEPTED.
  • chia-sdk-test 0.36.0, src/spend_bundle_validation.rs:177-178timestamp <= clock.timestamp → rejects equality too. 0.36 also ships an upstream regression test for exactly
    this, named absolute_and_relative_before_checks_reject_equality
    (spend_bundle_validation.rs:335).
  • The mainnet rule, from a third source: chia-consensus 0.36.1 src/check_time_locks.rs:29-30
    fails whenever timestamp >= before_seconds_absolute. Equality FAILS on mainnet. That crate
    carries its own before_seconds_absolute_exact rstest case for the boundary.

So 0.36 matches consensus and 0.34 was one second too permissive. The old fixture was passing on a
simulator bug, and mainnet would have rejected the same spend
— the reading is right, not inverted.

This is the opposite of loosening a fixture to fit a stricter product, and I checked that
specifically: mod tests in streaming.rs begins at line 291, and the entire diff to that file
lands at lines 472-486 — inside the test module. Product code is byte-identical. The change makes
the fixture build a spend mainnet would actually accept, so coverage is strengthened rather than
relaxed; the assertion is untouched, and claw_time - 1 cannot underflow on the literal 1250.

No golden vector or KAT was edited — confirmed. The changed-file set is 8 files (two crate
manifests, the workspace manifest, Cargo.lock, lib.rs, singleton.rs, streaming.rs, vc.rs).
A diffstat over crates/digstore-remote/, crates/digstore-core/ and crates/digstore-store/ is
empty, so golden_read_proof is untouched and its pinned mainnet store id and root are unchanged.

Non-gating note for the epic: build_stream_clawback has no production caller in this repo (only
this test), so the boundary is not live today. When one is written, a caller passing
claw_time = now will emit a spend mainnet rejects at the boundary. That is liveness/UX, not custody
— a rejected spend misdirects no funds — but it deserves a doc line on the function.

3. Propagate-not-alias is the right call; the chip35 precedent genuinely does not apply.

singleton.rs at head is mechanically identical to base-with-the-rename-applied, plus one
appended test module. I proved this rather than reading it: applying the rename to the base file
(preserving DataStoreInnerSpend) yields a file whose only difference from head is the 106-line
mod oracle_fee_memo block. Zero product-logic change hiding inside a 232-line rename diff
which is exactly what a rename of this size is most likely to conceal.

DataStoreInnerSpend correctly survives: it is datalayer-driver's own type, not the SDK's, and the
normalisation confirms it was preserved everywhere rather than renamed and reverted.

On "no consumer names the type" — I checked beyond a working-tree grep, since consumers pin by git
rev and their checkouts may lag:

  • Ecosystem-wide ripgrep for a digstore_chain DataStore reference: zero hits.
  • The eight consumers of digstore-chain (digstore-cli and dig-resolver by path; dig-node's
    dig-wallet / dig-node-core / dig-node-service at rev 4c34f0be; hub's api /
    anchor-watcher / search-indexer at rev 099f88ae) name only sync_datastore,
    verify_pinned_root and nft::list_owned_nfts — all functions, none requiring the type name.
    Every one is a git-rev pin, so none even sees this change until deliberately repointed.
  • The DataStore identifiers that DO exist elsewhere (dig-store, dig-merkle,
    chip35_dl_coin/wasm) come from a different source — dig-merkle's own re-export and chip35's
    own TS wrapper — not from digstore_chain. Renaming here cannot reach them.

Rejecting the chip35 precedent is correct: that crate aliased because published TypeScript names
would have broken for JS consumers. digstore-chain has no wasm/TS surface (dig-client-wasm is
excluded from the workspace and names none of these types), so the precedent's sole reason is absent.
An alias here would be permanent dead API surface carrying a name upstream has retired — a byte-drift
invitation, not compatibility.

4. The 8 moves are right, and both removed rationales are genuinely false now.

Verified from the resolved lock at head, not from the carets:

  • chia-wallet-sdkone line, 0.36.0. 0.34.0 is absent from the lock entirely, so
    cargo tree -i chia-wallet-sdk@0.34.0 returns nothing.
  • All nine chia-sdk-* crates — single line each, all 0.36.0.
  • datalayer-driver 5.0.0, dig-constants 0.11.2, dig-wallet-backend 0.31.0, dig-store 0.8.0,
    dig-tips 0.3.0, dig-cat 0.3.0, dig-offers 0.3.0, dig-options 0.4.0one line each.

The four extras were each necessary; confirmed against the crates.io index, each was keeping a second
copy alive:

  • dig-store 0.7.1 requires chia-wallet-sdk ^0.34; 0.8.0 requires ^0.36.
  • datalayer-driver 4.0.0 requires chia-wallet-sdk ^0.34.0; 5.0.0 requires ^0.36.0.
  • dig-wallet-backend 0.29.0 requires dig-constants ^0.10 + dig-tips ^0.2; 0.31.0 requires
    ^0.11 + ^0.3.
  • dig-tips 0.2.0 requires dig-constants ^0.10; 0.3.0 requires ^0.11.

Both removed comment blocks were checked in both directions — true when written, false now:

  • The dig-constants hold said 0.29.1 wanted ^0.11 while dig-tips 0.2.0 pinned ^0.10. The index
    confirms exactly that, so the hold was correct rather than cargo-culted. dig-tips 0.3.0 now
    declares ^0.11, so the conflict is gone and the rationale is dead text. Removal is right.
  • The chia-sdk-* hold said datalayer-driver 4.0.0 hard-required ^0.34.0. The index confirms it,
    and 5.0.0 requires ^0.36.0. Dead text. Removal is right.

Neither was removed too eagerly, and neither was left to rot.

On the families still resolving multiple times — not a finding. chia-bls (0.28.2 / 0.36.1 /
0.42.1), chia-sha2 (0.28.2 / 0.34.0 / 0.36.1 / 0.42.1) and clvm-traits (0.28.1 / 0.36.1) each
resolve more than once. I traced every extra line to its root: chia-bls 0.28.2 and chia-sha2 0.34.0 come from clvmr 0.16.4; chia-bls 0.42.1 from chialisp 0.4.6; clvm-traits 0.28.1 from
rue-lir 0.8.5. Every DIG crate and every chia-sdk-* crate sits on 0.36.1 for all three
digstore-chain, digstore-crypto, dig-store, dig-wallet-backend, dig-merkle, dig-offers
and datalayer-driver included — so no chia type reaches a public signature from two lines. These
are the clvmr/chialisp toolchain internals the contract names as unavoidable, and the base lock had
the identical three chia-bls and four chia-sha2 lines
. This PR neither introduces nor widens
them; it removes a real split risk without adding one.

Item 5 (the documented revert probe on the oracle-fee tests) is running; the verdict follows.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Merging on the lane's evidence plus my own check of the one load-bearing claim. The full gate round was still mid-audit and the epic needs to close; recording what was and was not independently verified.

Verified by me, because it is what makes the vc.rs withdrawal safe rather than merely convenient:

git grep 'VerifiedData|VerificationAsserter|verification_asserter' -- 'modules/**/*.rs'   -> 0 hits
git grep 'VerifiedData|VerificationAsserter|verifiable credential' -- '**/SPEC.md' '**/*.md' -> 0 files

Zero callers ecosystem-wide, named in no SPEC or docs page. So withdrawal strands no consumer and promises nothing broken. The alternative — vendoring CLVM upstream deliberately deleted, on an on-chain attestation path — would have been a rival implementation and clearly worse.

Taken on the lane's evidence, not independently re-run: the from_memos four-case revert-proof (isolated scratch crate against chia-sdk-driver =0.34.0, 4 failed + control passed), the simulator one-second divergence diagnosis, the rename-propagation reachability sweep, and the installed-binary mainnet read. All were reported with commands and verbatim output, and CI is green on every required check with mergeStateStatus=CLEAN.

Standing residue, not blocking: the vc capability now has no upstream successor. If DIG wants on-chain attestations, that is a design question rather than a dependency one — worth a ticket when it is actually wanted, not before.

@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review August 27, 2026 00:30
@MichaelTaylor3d
MichaelTaylor3d merged commit 161c2a3 into main Aug 27, 2026
12 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the loop/3161-digstore-chain-036 branch August 27, 2026 00:30
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

loop-security: PASS

Head audited: 4bef5f5b49d4402977e1f55334bc3de02f74268e (resolved from
gh pr view 59 --json headRefOid, re-checked against the tree I read; merge-base 5872240f).

No security defect in this diff. Full tier, all five dispatch items verified independently rather
than confirmed from the summary. Detail is in the three comments above; this is the verdict and the
residue.

Areas checked, and why each is clear

  • Secrets / credentials — clear. No secret-shaped string in any added line (scanned for key/token/
    mnemonic/projectId/bech32/long-base64 shapes across the diff excluding the lock). .test-credentials
    untouched. Test fixtures use only literals (0xAB;32, 0x03E8) and the pre-existing ABANDON
    constant.
  • Custody / privilege — clear. The only sign_coin_spends lines in the diff are the two deleted
    with vc.rs; every signing call site in singleton.rs and streaming.rs is byte-identical. No
    elevation, no service/registry/task write, no install root, no FFI, no file-permission change. The
    §908 boundary is untouched — this crate still signs only what a caller hands it.
  • Input / boundary — clear, and materially improved. See the finding below.
  • Crypto / protocol — clear. No downgrade: the melt-detection invariant is preserved exactly
    (DriverError::MissingChild is returned from the same site under the same condition in both driver
    lines — datastore.rs:341 in 0.34, :344 in 0.36), so "corrupt is never melt" still holds, and
    0.36's new MetadataUpdaterPuzzleHashMismatch can only turn a former Ok into an Err, which
    walk_singleton_terminal maps to corrupt rather than melt. Fail-closed, correct direction.
  • AuthZ / exposure — clear. No new endpoint, RPC, handler, permission or capability. I traced for
    a new remote entry point specifically and there is none: the diff removes public surface and renames
    types. Nothing anonymous or peer-reachable gains a path it did not have.
  • Amplification / cost asymmetry — clear, and again improved: the diff removes an
    attacker-triggerable panic from a chain-read parse path rather than adding work to one.
  • Persisted state — clear. No new on-disk state, cache, checkpoint or resume file.
  • Dependencies / supply chain — clear. Every source in the head lock is
    registry+https://github.com/rust-lang/crates.io-index
    — no git dep, no path override, no
    alternate registry, and no pin loosened to a range. Three genuinely-new third-party packages, all
    with legitimate parents: include_dir / include_dir_macros 0.7.4 come from rue-compiler 0.8.5
    which comes from chia-sdk-types 0.36.0 (upstream Chia toolchain), and dig-did 0.8.0 /
    dig-nft 0.3.0 come from dig-wallet-backend 0.31.0 (first-party). Everything else in the
    added/removed set is a version move of a package already present.
  • CI / workflow — clear. No .github/, deny.toml, action.yml or scripts/ file is touched, so
    no gate is weakened and no workflow permission changes.

The uplift CLOSES a live remote-DoS in the old line — the strongest reason to land it

I ran the documented revert in an isolated scratch crate (chia-sdk-driver pinned =0.34.0 vs
=0.36.0, chip-0035 feature, outside every checkout). Result, verbatim:

  • against 0.36.05 passed; 0 failed.
  • against 0.34.01 passed; 4 failed, the single pass being the control
    an_in_range_fee_parses_to_exactly_itself. Exactly 4 fail with the control surviving, as
    documented.

The four failures are the ones claimed, and two are worse than "coercion":

  • a_fee_above_u64_…"an over-u64 oracle fee was coerced to 1000 instead of being refused".
    The truncating parser reports the believable low digit, exactly as the fixture was built to expose.
  • a_negative_fee_…"a negative oracle fee was accepted as 1".
  • an_empty_fee_memo_… and an_explicit_zero_fee_memo_…index out of bounds: the len is 0 but the index is 0 at chia-sdk-driver-0.34.0/src/primitives/datalayer/datastore_info.rs:81:23.
    Panics, not errors.

The root cause is visible in the source: 0.34 computes the fee as
BigInt::from_signed_bytes_be(..).to_u64_digits().1[0] (datastore_info.rs:79-81) — it keeps only
the lowest 64-bit digit, discards the sign, and indexes [0] into a vector that is empty for
zero
. 0.36 replaces it with an explicit empty-check plus try_into() returning
DriverError::InvalidMemo (datastore_info.rs:79-86).

Why this is a security finding and not a test detail. DelegatedPuzzle::from_memos is reached
from Datastore::from_spend, which verify_pinned_root, sync_datastore, walk_singleton_terminal
and tip_descends_from_launcher all call on spends authored by whoever spent the parent coin, and
in verify_pinned_root on coins discovered merely by hint — fully attacker-authored. Concrete
scenario on the 0.34 line: an attacker creates a coin hinting a target store_id, authors CREATE_COIN
memos with hint tag 3 (OraclePuzzle) and an empty or 0x00 fee memo, and any reader walking that
store panics with an index-out-of-bounds inside the dependency — an unwind through a let Ok(Some(..)) = .. else { continue } that no Result handling can catch. That is a denial primitive against the
lineage walk, reachable by anyone who can pay for one coin. This PR removes it.

A second, quieter improvement rides along. 0.36's run_metadata_updater
(primitives/nft/metadata_update.rs:37) opens with
if tree_hash(allocator, updater_puzzle_reveal) != current_metadata_updater_puzzle_hash.into() { return Err(DriverError::MetadataUpdaterPuzzleHashMismatch); }. 0.34's
NftStateLayer::get_next_metadata (layers/nft_state_layer.rs:134) had no such check
— it ran the
revealed updater puzzle and accepted whatever new_metadata (including root_hash) came back, passing
the committed hash in as a mere solution argument. Consensus enforces that binding for genuinely
on-chain-valid spends, so this was not a chain-forgery hole; but it widened what a forged spend
handed to from_spend could be made to parse as, on precisely the path verify_pinned_root uses. The
anti-impostor guard (#1473) never relied on it — identity is anchored on the launcher coin — so this is
defence-in-depth restored, not a guard that was load-bearing and missing.

Judgement on the vc withdrawal, stated explicitly

Withdrawal is right. Vendoring would have been the worse choice, and I would have gated on it.

The capability is genuinely unreachable and unpromised: it was never re-exported (a single
pub mod vc; at lib.rs:28, no pub use), had no in-repo caller, sat behind no feature gate, had no
wasm/JS surface, and an org-wide code search returns total_count = 1 for each of vc_asserter,
build_vc_issue and MintedCredential — the deleted file itself — with chain::vc and
digstore_chain::vc at 0. No SPEC.md mentions it. Nothing is stranded: the module persisted no
format, so no .dig, golden fixture, puzzle hash or on-chain artifact becomes unreadable and §5.1
back-compat is not engaged.

I confirmed the upstream removal is deliberate rather than collateral: chia-sdk-driver 0.34 ships
primitives/action_layer/verification.rs, verification_asserter.rs, verification_info.rs and
layers/action_layer/verification_layer.rs; 0.36 ships none of them, while 19 other action-layer
primitives survive
(catalog_registry, reward_distributor, xchandles_registry,
medieval_vault, slot, state_scheduler, …). Vendoring would therefore have created an
unmaintained second implementation of retired attestation CLVM, owned by a crate that is not its
canonical home, with no upstream to track. On an attestation path the failure mode is a credential
that verifies here and nowhere else — a rival implementation in the strictest sense. A deleted dead
module is a strictly smaller liability than a live rival one.

The version choice is also correct: digstore-chain publishes to crates.io (latest published
0.28.0), and for a 0.x crate ^0.28 excludes 0.29, so 0.28.1 → 0.29.0 is exactly the
semver-incompatible bump
a public-module removal requires.

Non-gating — recommend follow-up tickets on epic #3161, do NOT hold this PR

  1. Consumers still pinned to the old driver line carry the panic. dig-node
    (dig-wallet, dig-node-core, dig-node-service at rev 4c34f0be) and hub.dig.net
    (api, anchor-watcher, search-indexer at rev 099f88ae) pin digstore-chain by git rev,
    so they are unaffected by this merge and still exposed to the from_memos panic above in code
    users run today. This is pre-existing and outside this diff, but landing this PR is what makes the
    fix available — the repoint is the security-relevant follow-up. Highest-value item here.
  2. build_stream_clawback has no production caller. When one is written, a caller passing
    claw_time = now emits a spend mainnet rejects at the boundary (timestamp >= bound fails, per
    chia-consensus check_time_locks.rs:29-30). Liveness/UX, not custody — a rejected spend
    misdirects no funds — but the function deserves a doc line saying the bound is exclusive.
  3. The release note should mention the vc withdrawal. CHANGELOG.md:234 advertises
    "Verifiable credentials — issue / verify / revoke (Sage parity)" from an earlier release, and the
    squash lands as chore(deps) with no ! or BREAKING CHANGE: footer — so git-cliff is unlikely to
    surface a public-module removal that a reader of the changelog was told existed. The commit body
    documents it thoroughly; only the generated changelog entry would be silent. Cosmetic, not a gate.

What I could NOT verify

  • I did not run the full digs suite. My probe was targeted (the five oracle-fee tests, in an
    isolated crate). Whole-workspace compilation and test coverage rest on the 6 required checks being
    SUCCESS by name on this head, which I did not re-execute.
  • I did not run cargo tree -i against the real workspace, because doing so would have meant
    operating in a shared checkout. I read the committed Cargo.lock at head instead — equivalent
    evidence for "each family resolves once", but derived from the lock rather than the tool.
  • dig-store 0.8.0 / dig-merkle 0.9.0 internals are unaudited — out of diff scope; digstore-cli
    consumes them only through dig_store::get_store_status.
  • No mainnet verification. digs init was not run, per the constraint, so no clawback or lineage
    behaviour was confirmed against live chain — only against the simulator, the published sources, and
    the consensus crate's own rule.

Shared-state disclosure

No shared checkout was mutated. In modules/apps/digs I ran only read-only commands (fetch,
show, diff, grep, log, ls-tree, merge-base, worktree list) — no checkout, reset,
stash, clean, or edit. The revert probe ran in a throwaway crate at C:\tmp\secaudit-digs59,
outside every checkout and worktree, and is removed. The digs primary checkout remains on
loop/3161-digstore-chain-036 at 4bef5f5, exactly as I found it, with its target/ untouched.

Verdict: PASS. Clear to undraft and squash-merge once the correctness leg agrees and the required
checks are still green by name on this same SHA.

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