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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,15 @@ Create a BIP-39 seed wallet encrypted on disk. The `<name>` argument is a single

**New seed:** records the current chain tip in `.stealth-start-block` so later `balances` stealth scans do not walk announcement history from before the wallet existed. Uses `--rpc-url` / `RPC_URL` when set; otherwise a public RPC for mainnet or Sepolia (`--testnet`).

**Import (`--import`):** scans used public HD indexes via RPC. Writes `.stealth-start-block` so the first `balances` stealth scan starts at the Kohaku-schema floor (mainnet `25700000`, Sepolia `11455454`) unless you pass `--stealth-start-block`. That flag can still go back as far as the ERC-5564 announcer deploy block.
**Import (`--import`):** scans used public HD indexes via RPC. Writes the current chain tip to `.stealth-start-block` (same as a new seed) so the first `balances` stealth scan does not walk historical announcements. Pass `--stealth-start-block` with no value to opt into the Kohaku-schema floor (mainnet `25700000`, Sepolia `11455454`), or `--stealth-start-block <block>` for an explicit floor (values below the ERC-5564 announcer deploy are rounded up to that block). Later, `balances --stealth-start-block` can still back-date below whatever was written at import.

| Option | Description |
|--------|-------------|
| `--testnet` | Tag wallet for Sepolia instead of mainnet. |
| `--import` | Restore from mnemonic instead of generating a new one. |
| `--long-seed` | Generate a 24-word (256-bit) mnemonic instead of the default 12-word (128-bit). Ignored with `--import`. |
| `--rpc-url <url>` | Required with `--import` (or `RPC_URL`) to scan used addresses. Optional for new wallets when writing `.stealth-start-block`. |
| `--stealth-start-block <block>` | With `--import`: write `.stealth-start-block` for later `balances` stealth scans. Default when omitted: mainnet `25700000`, Sepolia `11455454`. New wallets set this automatically from the current tip. |
| `--stealth-start-block [block]` | With `--import`: write `.stealth-start-block`. Omit the flag to record the current tip (same as a new wallet). Bare flag: Kohaku floor (mainnet `25700000`, Sepolia `11455454`). With a number: that block, rounded up to the ERC-5564 announcer deploy if lower. |
| `--mnemonic <phrase>` | Mnemonic (required with `--non-interactive --import`). |
| `--password <password>` | Encryption password (required with `--non-interactive`). |
| `--non-interactive` | No prompts; no mnemonic box on create. |
Expand All @@ -190,6 +190,7 @@ Create a BIP-39 seed wallet encrypted on disk. The `<name>` argument is a single
kohaku create-wallet myWallet --testnet
kohaku create-wallet myWallet24 --testnet --long-seed
kohaku create-wallet restored --testnet --import --rpc-url "$RPC_URL"
kohaku create-wallet restored --testnet --import --rpc-url "$RPC_URL" --stealth-start-block
kohaku create-wallet restored --testnet --import --rpc-url "$RPC_URL" --stealth-start-block 10000000
```

Expand Down Expand Up @@ -322,7 +323,7 @@ By default, private balances are included only for `DEFAULT_PRIVACY_PROTOCOL` (i
| `--verbose` | Human: per-address public breakdown + private note list for included protocols. JSON: adds `public_account_indexes_by_address` and `private_notes`. |
| `--tokensList <addrs>` | Extra ERC-20 addresses (comma- or space-separated), merged with chain defaults. |
| `--without-tor` | Disable Tor for privacy HTTP when syncing private protocols (default: Tor on). Covers Railgun Subsquid/PPOI, Tornado saga/artifacts, Privacy Pools ASP/fastrelay, etc. RPC stays clearnet. Or set `KOHAKU_WITHOUT_TOR=1`. |
| `--stealth-start-block <block>` | Floor for the ERC-5564 announcement **scan** (decimal or `0x`-hex); skips older history on first/full scan. Can back-date below the wallet `.stealth-start-block` (as far as the announcer deploy block). When omitted, uses that file if present, otherwise the Kohaku import default (mainnet `25700000`, Sepolia `11455454`). Not a way to skip scanning. |
| `--stealth-start-block <block>` | Floor for the ERC-5564 announcement **scan** (decimal or `0x`-hex); skips older history on first/full scan. Can back-date below the wallet `.stealth-start-block` (often the creation/import tip; as far as the announcer deploy block). When omitted, uses that file if present, otherwise the Kohaku import default (mainnet `25700000`, Sepolia `11455454`). Not a way to skip scanning. |
| `--skip-stealth-scan` | Skip announcement discovery for this run. **Already-imported** stealth accounts still appear in public balances. |
| `--non-interactive` | JSON only; requires `--wallet` and `--password`. |
| `--dataDir <path>` | Data root. |
Expand Down Expand Up @@ -842,7 +843,7 @@ Files include `public-accounts.json`, stealth storage, `rg-storage.json`, `ppv1-
- **Dry run vs broadcast:** `transfer`, `transact-raw`, `shield`, `unshield`, `init-profile`, and the name commands default to *prepare or simulate only*. Always read the printed transaction data before adding `--broadcast`.
- **Tor (all-but-RPC):** Non-RPC HTTP (Pimlico, Railgun Subsquid/PPOI, Tornado saga/artifacts, Privacy Pools ASP/fastrelay, …) goes through [tor-js](https://github.com/privacy-ethereum/tor-js) by default on `balances` (when syncing private protocols), `shield`, `unshield`, Tornado note import/export, `transfer`, `transact-raw`, and name commands. Ethereum RPC stays clearnet. Use `--without-tor` or `KOHAKU_WITHOUT_TOR=1` to skip. **Saga CDN and proving artifacts are Tor-or-fail** (no clearnet fallback; large GETs time out after `KOHAKU_TOR_CDN_TIMEOUT_MS`, default 45s). First protocol sync (saga / Subsquid / ASP / RPC catch-up) shows live progress on the spinner and does not download proving keys. Artifacts are served from `<dataDir>/proving-artifacts` when cached; otherwise fetched from `KOHAKU_ARTIFACTS_BASE_URL` (default: `https://artifacts.0000000000.org`) on **prove / unshield**. Pre-warm keys with `kohaku fetch-artifacts` (optionally `--without-tor` for a one-shot clearnet download). Prefetch historical Subsquid/saga pages with `kohaku fetch-sync-cache`, which pulls a chunked, `sha256`-verified snapshot one piece at a time. After Tor bootstrap corruption, run `kohaku clear-tor-cache`. Railgun Subsquid and Tornado saga HTTP pages are reused from `<dataDir>/public-sync-cache` (wipe with `kohaku clear-tor-cache --public-sync`); Privacy Pools ASP and RPC are always live. Set `KOHAKU_TOR_DEBUG=1` for per-request Tor logs. A keyed RPC URL still identifies you to that provider regardless of Tor. Review with `view-network-traffic --wallet <name>`.
- **Fresh addresses:** Use `next-fresh-address` before funding, and `unshield --next` when you want withdrawals to land on a new public key that was not your shield source. Use `next-fresh-address --peek` to see the next address without persisting it (e.g. when building `--tail-calls` for a later `unshield --next`). Do not pass a peeked address as Tornado `--to` together with `--tail-calls` — peeked addresses are not stored, so the CLI will refuse rather than 7702 a note-derived key.
- **Profile / stealth:** Prefer `init-profile` to publish ERC-6538 keys (and optionally a name). Unshield to stored stealth accounts with `--to s0`. Print the meta URI with `see-stealth-meta-address`. New wallets store `.stealth-start-block` at creation so first `balances` stealth scans skip pre-wallet announcer history; imports default to mainnet `25700000` / Sepolia `11455454` (Kohaku-schema floor) and can still pass `--stealth-start-block` to start earlier.
- **Profile / stealth:** Prefer `init-profile` to publish ERC-6538 keys (and optionally a name). Unshield to stored stealth accounts with `--to s0`. Print the meta URI with `see-stealth-meta-address`. New wallets and imports store `.stealth-start-block` at the current chain tip so first `balances` stealth scans skip pre-wallet announcer history. Pass `create-wallet --import --stealth-start-block` (bare) for the Kohaku-schema floor (mainnet `25700000` / Sepolia `11455454`), or an explicit block. Later `balances --stealth-start-block` can still back-date below the file.
- **Privacy Pools note size:** Each unshield uses one note; large shields may require multiple unshields if balances are split across notes.
- **Tornado notes:** Use `export-tornado-note` / `import-tornado-note` to move legacy note secrets between wallets for testing or recovery.
- **Private key / seed exports:** `export-private-key`, `reveal-seed-phrase`, and `export-tornado-note` print raw secrets to stdout. Avoid terminal logs, shell history, and shared environments.
Expand Down
13 changes: 8 additions & 5 deletions src/commands/createWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { cliError, cliErrorFromCaught } from "../utils/cli-errors";
import {
createWalletOnDisk,
generateMnemonic,
interpretImportStealthStartBlockFlag,
} from "../lib/create-wallet";
import { parseStealthStartBlock } from "../lib/stealth/scan.js";
import {
DEFAULT_DATA_DIR,
resolveOptionalRpcUrl,
Expand All @@ -30,7 +30,7 @@ type CreateWalletOpts = {
rpcUrl?: string;
testnet?: boolean;
longSeed?: boolean;
stealthStartBlock?: string;
stealthStartBlock?: string | true;
dataDir?: string;
};

Expand Down Expand Up @@ -99,8 +99,8 @@ export function registerCreateWalletCommand(program: Command): void {
.option("--rpc-url <url>", "RPC URL (or set RPC_URL). Optional for new wallets: a public RPC is used to record the current block if unset. Required with --import")
.option("--testnet", "Use testnet chain ID (11155111) instead of mainnet (1)")
.option(
"--stealth-start-block <block>",
"With --import: write `.stealth-start-block` for later balances stealth scans (default: mainnet 25700000, Sepolia 11455454). New wallets record the current tip automatically."
"--stealth-start-block [block]",
"With --import: write `.stealth-start-block`. Bare flag = Kohaku floor (mainnet 25700000, Sepolia 11455454). With a number, use that block (rounded up to the ERC-5564 announcer deploy if lower). Omit the flag to record the current tip (same as a new wallet)."
)
.option(
"--long-seed",
Expand Down Expand Up @@ -181,7 +181,10 @@ export function registerCreateWalletCommand(program: Command): void {
let stealthStartBlock: bigint | undefined;
if (opts.stealthStartBlock !== undefined) {
try {
stealthStartBlock = parseStealthStartBlock(opts.stealthStartBlock);
stealthStartBlock = interpretImportStealthStartBlockFlag(
opts.stealthStartBlock,
opts.testnet ? 11155111n : 1n
);
} catch (e) {
cliErrorFromCaught(e);
return;
Expand Down
37 changes: 31 additions & 6 deletions src/lib/create-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {
writeWalletType,
} from "../utils/wallets-util.js";
import { defaultStealthImportStartBlock } from "./stealth/constants.js";
import {
parseStealthStartBlock,
resolveStealthScanFloor,
} from "./stealth/scan.js";
import { writeStealthStartBlock } from "./stealth/start-block-file.js";

export async function findLastTouchedPublicIndex(
Expand Down Expand Up @@ -68,12 +72,32 @@ export type CreateWalletOnDiskInput = {
rpcUrl?: string;
/**
* Import only: persist as `.stealth-start-block` for later `balances` scans.
* When omitted, writes the Kohaku-schema floor for the chain. New (generated)
* wallets record the current chain tip automatically instead.
* When omitted, records the current chain tip (same as a new wallet). The
* command layer passes the Kohaku-schema floor when the user gives a bare
* `--stealth-start-block`, or an explicit block when they pass a number.
*/
stealthStartBlock?: bigint;
};

/**
* Interpret `create-wallet --import --stealth-start-block [block]`.
* Omitted → undefined (caller writes the chain tip). Bare flag → Kohaku floor.
* Explicit `n` is rounded up to the ERC-5564 announcer deploy block when below it.
*/
export function interpretImportStealthStartBlockFlag(
flag: string | true | undefined,
chainId: bigint
): bigint | undefined {
if (flag === undefined) return undefined;
if (flag === true || flag === "") {
return defaultStealthImportStartBlock(chainId);
}
return resolveStealthScanFloor({
chainId,
startFromBlock: parseStealthStartBlock(flag),
});
}

export async function createWalletOnDisk(
input: CreateWalletOnDiskInput
): Promise<{
Expand Down Expand Up @@ -122,10 +146,11 @@ export async function createWalletOnDisk(
writeWalletType(input.testnet ? "testnet" : "mainnet", walletDir);

let stealthStartBlockWritten: bigint | undefined;
if (input.importMode) {
const block =
input.stealthStartBlock ??
defaultStealthImportStartBlock(expectedChainId);
if (input.stealthStartBlock !== undefined) {
const block = resolveStealthScanFloor({
chainId: expectedChainId,
startFromBlock: input.stealthStartBlock,
});
writeStealthStartBlock(walletDir, block);
stealthStartBlockWritten = block;
} else {
Expand Down
7 changes: 4 additions & 3 deletions src/lib/stealth/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ export function stealthAnnouncerStartBlock(chainId: bigint): bigint {
/**
* Optimistic first-scan floor for Kohaku-schema stealth keys.
*
* The ERC-5564 announcer is older than this CLI's derivation scheme; imports
* that omit `--stealth-start-block` start here instead of at contract deploy.
* `balances --stealth-start-block` can still back-date down to
* The ERC-5564 announcer is older than this CLI's derivation scheme.
* `create-wallet --import --stealth-start-block` (no block number) writes this
* floor into `.stealth-start-block`. `balances` uses it when that file is
* missing. `balances --stealth-start-block` can still back-date down to
* {@link stealthAnnouncerStartBlock}.
*/
export function defaultStealthImportStartBlock(chainId: bigint): bigint {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/cli-command-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ export const cliOptions = {
withoutTorSyncCacheFetch:
"Download snapshot chunks over clearnet (default: Tor). Much faster, and reveals only that this IP fetched public Railgun/Tornado pool data. Or set KOHAKU_WITHOUT_TOR=1",
stealthStartBlock:
"Start ERC-5564 announcement scan at this block (decimal or 0x-hex); skips older history on first/full scan. Can back-date below the wallet `.stealth-start-block` (down to the announcer deploy block). When omitted, uses that file, or the Kohaku import default. Does not skip the scan itself (use --skip-stealth-scan)",
"Start ERC-5564 announcement scan at this block (decimal or 0x-hex); skips older history on first/full scan. Can back-date below the wallet `.stealth-start-block` (often the creation/import tip; down to the announcer deploy block). When omitted, uses that file, or the Kohaku import default. Does not skip the scan itself (use --skip-stealth-scan)",
} as const;
74 changes: 74 additions & 0 deletions tests/create-wallet-stealth-start.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";

import { interpretImportStealthStartBlockFlag } from "../src/lib/create-wallet.js";
import {
defaultStealthImportStartBlock,
stealthAnnouncerStartBlock,
} from "../src/lib/stealth/constants.js";

const MAINNET = 1n;
const SEPOLIA = 11155111n;

describe("interpretImportStealthStartBlockFlag", () => {
it("omitted flag means the caller should write the chain tip", () => {
assert.equal(
interpretImportStealthStartBlockFlag(undefined, MAINNET),
undefined
);
assert.equal(
interpretImportStealthStartBlockFlag(undefined, SEPOLIA),
undefined
);
});

it("bare flag uses the Kohaku-schema floor for the chain", () => {
assert.equal(
interpretImportStealthStartBlockFlag(true, MAINNET),
defaultStealthImportStartBlock(MAINNET)
);
assert.equal(
interpretImportStealthStartBlockFlag(true, SEPOLIA),
defaultStealthImportStartBlock(SEPOLIA)
);
assert.equal(
interpretImportStealthStartBlockFlag("", MAINNET),
defaultStealthImportStartBlock(MAINNET)
);
});

it("parses an explicit decimal or 0x-hex block at or above deploy", () => {
assert.equal(
interpretImportStealthStartBlockFlag("30000000", MAINNET),
30_000_000n
);
assert.equal(
interpretImportStealthStartBlockFlag("0x1000000", SEPOLIA),
0x1000000n
);
});

it("rounds an explicit block up to the announcer deploy floor", () => {
const mainnetDeploy = stealthAnnouncerStartBlock(MAINNET);
const sepoliaDeploy = stealthAnnouncerStartBlock(SEPOLIA);
assert.equal(
interpretImportStealthStartBlockFlag("0", MAINNET),
mainnetDeploy
);
assert.equal(
interpretImportStealthStartBlockFlag("10000000", MAINNET),
mainnetDeploy
);
assert.equal(
interpretImportStealthStartBlockFlag("0x1", SEPOLIA),
sepoliaDeploy
);
});

it("rejects garbage explicit values", () => {
assert.throws(
() => interpretImportStealthStartBlockFlag("latest", MAINNET),
/decimal or 0x-hex/
);
});
});