Task
Build the offline seed-export CLI — step 3 of https://github.com/DIG-Network/dig_ecosystem/issues/1701.
Node-side USER custody is being retired. For a user who migrated their seed into this node and kept
no independent copy, the node's seed file is the only surviving copy of their spend key, so the
custody code cannot be deleted until that key has been handed back. This command is that hand-back,
and it is deleted along with the custody surface.
Step 3 was previously skipped on a population count of zero. That count was scoped to the wrong
store. The real population is one, and it is a user's wallet.
Context
The measured blob:
| property |
value |
| size |
248 bytes |
| first byte |
0x01 — the legacy EncryptedSeed layout |
| sealed under |
a user password (no device key for that base) |
| funded state |
unknown (wallet.meta.json absent) |
| status |
already orphaned — current builds resolve a different base directory |
Five other blobs exist and are not population: they are operator identity (DIGOP1, device key,
never funded). They are not touched, not exported, not counted.
seed_path() resolves {LOCALAPPDATA || HOME || "."}/DigWallet/seed.bin
(crates/dig-wallet/src/lib.rs:200).
The two traps
- It must decrypt the LEGACY format. An export built on
dig_keystore::opaque::open fails on
100% of the real population. It must go through seed_store::decrypt_seed, which dispatches on
the leading magic.
- It must accept an explicit path. The blob is not where
seed_path() resolves on that machine,
so a resolver-only command cannot see the one thing it exists to rescue.
Scope — do not widen it
dig-node wallet export-seed:
- requires local filesystem access plus the wallet password — the same two things an attacker
would already need
- prints the mnemonic to the console
- no network surface, no RPC method, no loopback endpoint. A served
wallet.export_seed was
considered and REJECTED on the parent ticket: it would permanently add a seed-exfiltration
capability to the loopback surface, reachable by anything holding a paired token or an mTLS client
certificate, to solve a one-time migration. That ruling stands.
- is deleted in step 4 with the custody code
Read-only: no blob is deleted, moved, zeroized or overwritten. POST /api/export stays — it is
currently the sole recovery path in the workspace. seed_path(), seed_store and autoseed are
not modified; five live operator identities depend on them.
Evidence
Legacy 0x01 decrypts, proven by mutation (building on opaque::open must fail the legacy test by
name); an explicit path reaches a non-default location; a wrong password fails cleanly; the mnemonic
reaches no log and no error string.
Limit
The parent's acceptance is export → import into dig-app → derived address matches → only then the
node blob is zeroized. That is a human-observed round trip, not a unit test, and it remains
outstanding after this ticket.
Parent
https://github.com/DIG-Network/dig_ecosystem/issues/1701
Siblings
Task
Build the offline seed-export CLI — step 3 of https://github.com/DIG-Network/dig_ecosystem/issues/1701.
Node-side USER custody is being retired. For a user who migrated their seed into this node and kept
no independent copy, the node's seed file is the only surviving copy of their spend key, so the
custody code cannot be deleted until that key has been handed back. This command is that hand-back,
and it is deleted along with the custody surface.
Step 3 was previously skipped on a population count of zero. That count was scoped to the wrong
store. The real population is one, and it is a user's wallet.
Context
The measured blob:
0x01— the legacyEncryptedSeedlayoutwallet.meta.jsonabsent)Five other blobs exist and are not population: they are operator identity (
DIGOP1, device key,never funded). They are not touched, not exported, not counted.
seed_path()resolves{LOCALAPPDATA || HOME || "."}/DigWallet/seed.bin(
crates/dig-wallet/src/lib.rs:200).The two traps
dig_keystore::opaque::openfails on100% of the real population. It must go through
seed_store::decrypt_seed, which dispatches onthe leading magic.
seed_path()resolves on that machine,so a resolver-only command cannot see the one thing it exists to rescue.
Scope — do not widen it
dig-node wallet export-seed:would already need
wallet.export_seedwasconsidered and REJECTED on the parent ticket: it would permanently add a seed-exfiltration
capability to the loopback surface, reachable by anything holding a paired token or an mTLS client
certificate, to solve a one-time migration. That ruling stands.
Read-only: no blob is deleted, moved, zeroized or overwritten.
POST /api/exportstays — it iscurrently the sole recovery path in the workspace.
seed_path(),seed_storeandautoseedarenot modified; five live operator identities depend on them.
Evidence
Legacy
0x01decrypts, proven by mutation (building onopaque::openmust fail the legacy test byname); an explicit path reaches a non-default location; a wrong password fails cleanly; the mnemonic
reaches no log and no error string.
Limit
The parent's acceptance is export → import into dig-app → derived address matches → only then the
node blob is zeroized. That is a human-observed round trip, not a unit test, and it remains
outstanding after this ticket.
Parent
https://github.com/DIG-Network/dig_ecosystem/issues/1701
Siblings