Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ edition = "2021"
# the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a
# release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet)
# keep their own independent versions — only the released binary tracks the workspace version.
version = "0.144.1"
version = "0.145.0"

# Release hardening, matching digstore: keep integer-overflow checks ON in release.
# The node parses untrusted serialized input and does offset/length arithmetic over
Expand Down
10 changes: 10 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -2428,6 +2428,16 @@ on-disk master token = local-machine control), never an unauthenticated backdoor
`control.wallet.peak`; `wallet broadcast <signed_bundle_hex>` → `control.wallet.broadcast`. The
open chain reads (everything above except `broadcast` and `arrivals`) need no token; `broadcast` is token-gated like every
other mutation, and carries only already-signed bytes (§908).
- `wallet export-seed [--path <file>]` reaches NO control method. It is a LOCAL, OFFLINE read of
this node's encrypted seed file: it decrypts the file under the wallet password supplied on the
terminal and prints the recovery phrase to stdout. It opens no socket, adds no `control.*` method
and adds no loopback endpoint, so it grants nothing beyond what local filesystem access plus the
password already grant. It accepts BOTH on-disk seed formats: the current `dig-keystore` container
and the legacy `EncryptedSeed` layout (leading version byte `1`) that pre-migration files use.
`--path` overrides the default location, because a file written by an older build can sit under a
base directory the current build no longer resolves. `--json` is REFUSED: a recovery phrase must
not be emitted as machine-readable output. The command exists only to let a user move a
node-custodied wallet out before node-side user custody is removed, and is deleted with it.
- `updater [status]` → `control.updater.status`; `updater set-channel <ch>` / `pause [--until <s>]`
/ `resume` / `check-now` → the matching `control.updater.*`.
- `subscriptions [list]` → `control.listSubscriptions`; `subscriptions add|remove <store_id>` →
Expand Down
5 changes: 5 additions & 0 deletions crates/dig-node-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ path = "src/lib.rs"
testkit = []

[dependencies]
# Reads the wallet password for the offline `wallet export-seed` rescue without echoing it
# to the terminal. Used ONLY there; it is removed with that command when node-side wallet
# custody goes. The alternative -- an env var or an echoed stdin line -- would put a spend
# key into shell history, a process listing, or a scrollback buffer.
rpassword = "7"
# The canonical NODE engine library (crate `dig_node_core`) — a first-party sibling
# crate in this workspace (no longer a stale git-pinned digstore alias). It IS the
# rpc.dig.net-compatible node the native DIG Browser runs in-process (`handle_rpc`):
Expand Down
60 changes: 49 additions & 11 deletions crates/dig-node-service/src/entrypoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
//! documented in [`crate::cli`] and the README.

use std::ffi::OsStr;
use std::path::Path;
use std::path::{Path, PathBuf};

use crate::seed_export_cli;

use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};

Expand Down Expand Up @@ -363,6 +365,21 @@ enum WalletCommand {
},
/// Print the public keys this node is currently following (READ-ONLY).
Watched,
/// Print the recovery phrase of a wallet this node still holds, so you can move it
/// into the DIG app before node-side wallet custody is removed.
///
/// LOCAL AND OFFLINE. It reads the seed file on this machine and needs that wallet's
/// password; it contacts no node, opens no port, and adds nothing to the node's network
/// surface. It prints the phrase to the console, so run it where nobody can read your
/// screen, and never into a file or a log.
///
/// A phrase is the whole wallet: anyone who reads it can spend those funds.
ExportSeed {
/// Read this seed file instead of the default location. An older build may have
/// written yours elsewhere; the error text names the path that was tried.
#[arg(long)]
path: Option<PathBuf>,
},
}

/// `dig-node profile` sub-actions — the two halves of profile-body custody on this node.
Expand Down Expand Up @@ -654,9 +671,22 @@ pub fn run() -> std::process::ExitCode {
Command::Sync { action: cmd } => {
render(control_cli::run(&config, sync_action(cmd)), action, json)
}
Command::Wallet { action: cmd } => {
render(control_cli::run(&config, wallet_action(cmd)), action, json)
}
// `export-seed` is the one wallet verb that reaches no node: it is a local,
// offline read of the seed file, so it never goes near `control_cli`.
Command::Wallet {
action: WalletCommand::ExportSeed { path },
} => seed_export_cli::run(path, json),
Command::Wallet { action: cmd } => match wallet_action(cmd) {
Some(control) => render(control_cli::run(&config, control), action, json),
// Every LOCAL wallet verb must be routed in an arm above. Today `export-seed`
// is the only one and it is, so this cannot fire; it degrades to a usage error
// rather than a panic so that adding a local verb and forgetting to route it
// misbehaves visibly instead of aborting the process.
None => {
eprintln!("error: this wallet verb is local-only and was not routed");
ExitCode::Usage
}
},
Command::Profile { action: cmd } => {
render(control_cli::run(&config, profile_action(cmd)), action, json)
}
Expand Down Expand Up @@ -725,8 +755,8 @@ fn sync_action(cmd: Option<SyncCommand>) -> ControlAction {
}

/// Map the `wallet` subcommand to its [`ControlAction`] (#1851, dig_ecosystem#2376).
fn wallet_action(cmd: WalletCommand) -> ControlAction {
match cmd {
fn wallet_action(cmd: WalletCommand) -> Option<ControlAction> {
Some(match cmd {
WalletCommand::Balance { address, asset } => {
ControlAction::WalletBalance { address, asset }
}
Expand All @@ -753,7 +783,9 @@ fn wallet_action(cmd: WalletCommand) -> ControlAction {
WalletCommand::Watch { public_keys } => ControlAction::WalletWatch { public_keys },
WalletCommand::Unwatch { public_keys } => ControlAction::WalletUnwatch { public_keys },
WalletCommand::Watched => ControlAction::WalletWatched,
}
// Handled locally before this mapping is reached; it names no control method.
WalletCommand::ExportSeed { .. } => return None,
})
}

/// Map the `profile` subcommand to its [`ControlAction`].
Expand Down Expand Up @@ -1123,7 +1155,7 @@ mod tests {
/// that is the whole point of these tests (see below).
fn method_for_argv(argv: &[&str]) -> Option<&'static str> {
match Cli::try_parse_from(argv).ok()?.command? {
Command::Wallet { action } => Some(wallet_action(action).method()),
Command::Wallet { action } => Some(wallet_action(action)?.method()),
_ => None,
}
}
Expand Down Expand Up @@ -1304,7 +1336,9 @@ mod tests {
panic!("parsed to something other than `wallet`");
};
assert_eq!(
wallet_action(action).wire_params(),
wallet_action(action)
.expect("a control-plane wallet verb maps to an action")
.wire_params(),
serde_json::json!({ "address": address, "asset": "dig" }),
"the address and the non-default asset must both survive the mapping"
);
Expand All @@ -1323,7 +1357,9 @@ mod tests {
panic!("parsed to something other than `wallet`");
};
assert_eq!(
wallet_action(action).wire_params(),
wallet_action(action)
.expect("a control-plane wallet verb maps to an action")
.wire_params(),
serde_json::json!({ "after_seq": 17, "limit": 3 }),
"the cursor the caller resumed from must be the cursor that is asked for"
);
Expand All @@ -1334,7 +1370,9 @@ mod tests {
panic!("parsed to something other than `wallet`");
};
assert_eq!(
wallet_action(action).wire_params(),
wallet_action(action)
.expect("a control-plane wallet verb maps to an action")
.wire_params(),
serde_json::json!({ "signed_bundle_hex": "0xfeed" }),
"the bundle the operator typed must be the bundle that is pushed"
);
Expand Down
4 changes: 4 additions & 0 deletions crates/dig-node-service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ pub mod peers;
/// upstream, and the bring-up probe that proves an upstream is not this node itself. See [`relay`].
pub mod relay;
pub mod rpc;
/// The offline `wallet export-seed` rescue command: a local read of this node's
/// encrypted seed file. Adds no network surface, and is removed with node-side custody.
pub mod seed_export_cli;

/// Shared OS-owner trust gate ([`security::dir_is_privileged`]): is a directory owned by a
/// privileged principal (SYSTEM/Administrators or root) and not user-writable? Used by the self-heal
/// spawn root (#565) and the TLS material root (#661) so the one Win32/unix owner check lives once.
Expand Down
Loading
Loading