From 1648c8c9e2b7bbe7db0f5445235b753f4a8e642b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=BA=CE=B1=CF=83=CF=83=CE=AC=CE=BD=CE=B4=CF=81=CE=B1=2Ee?= =?UTF-8?q?th?= <0xDADA@protonmail.com> Date: Wed, 26 Aug 2026 09:26:49 -0400 Subject: [PATCH 1/2] chore: cleanup stealth import --- CHANGELOG.md | 6 ++ README.md | 9 +-- src/commands/createWallet.ts | 13 ++-- src/lib/create-wallet.ts | 37 ++++++++++-- src/lib/stealth/constants.ts | 7 ++- src/utils/cli-command-options.ts | 2 +- tests/create-wallet-stealth-start.test.ts | 74 +++++++++++++++++++++++ 7 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 tests/create-wallet-stealth-start.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 016add8..b464fc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [Unreleased] + +### Changed + +- `create-wallet --import` now writes the current chain tip to `.stealth-start-block` (same as a new wallet) so the first stealth scan does not walk historical announcements. Pass `--stealth-start-block` with no value for the previous Kohaku-schema floor, or `--stealth-start-block ` for an explicit floor. `balances --stealth-start-block` can still back-date below the file. + ## [0.0.4] — 2026-08-25 ### Added diff --git a/README.md b/README.md index 70f886d..e896c6c 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ Create a BIP-39 seed wallet encrypted on disk. The `` 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 ` 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 | |--------|-------------| @@ -176,7 +176,7 @@ Create a BIP-39 seed wallet encrypted on disk. The `` argument is a single | `--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 ` | Required with `--import` (or `RPC_URL`) to scan used addresses. Optional for new wallets when writing `.stealth-start-block`. | -| `--stealth-start-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 ` | Mnemonic (required with `--non-interactive --import`). | | `--password ` | Encryption password (required with `--non-interactive`). | | `--non-interactive` | No prompts; no mnemonic box on create. | @@ -190,6 +190,7 @@ Create a BIP-39 seed wallet encrypted on disk. The `` 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 ``` @@ -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 ` | 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 ` | 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 ` | 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 ` | Data root. | @@ -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 `/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 `/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 `. - **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. diff --git a/src/commands/createWallet.ts b/src/commands/createWallet.ts index c6e36b1..99d1f92 100644 --- a/src/commands/createWallet.ts +++ b/src/commands/createWallet.ts @@ -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, @@ -30,7 +30,7 @@ type CreateWalletOpts = { rpcUrl?: string; testnet?: boolean; longSeed?: boolean; - stealthStartBlock?: string; + stealthStartBlock?: string | true; dataDir?: string; }; @@ -99,8 +99,8 @@ export function registerCreateWalletCommand(program: Command): void { .option("--rpc-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 ", - "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", @@ -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; diff --git a/src/lib/create-wallet.ts b/src/lib/create-wallet.ts index 1f019e9..ededefe 100644 --- a/src/lib/create-wallet.ts +++ b/src/lib/create-wallet.ts @@ -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( @@ -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<{ @@ -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 { diff --git a/src/lib/stealth/constants.ts b/src/lib/stealth/constants.ts index ccedf48..54ba67a 100644 --- a/src/lib/stealth/constants.ts +++ b/src/lib/stealth/constants.ts @@ -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 { diff --git a/src/utils/cli-command-options.ts b/src/utils/cli-command-options.ts index 39beaa1..66cb4e4 100644 --- a/src/utils/cli-command-options.ts +++ b/src/utils/cli-command-options.ts @@ -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; diff --git a/tests/create-wallet-stealth-start.test.ts b/tests/create-wallet-stealth-start.test.ts new file mode 100644 index 0000000..977d7f5 --- /dev/null +++ b/tests/create-wallet-stealth-start.test.ts @@ -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/ + ); + }); +}); From d1cfcb4e31f9b0de5b3a070ebd6d09e697237d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=BA=CE=B1=CF=83=CF=83=CE=AC=CE=BD=CE=B4=CF=81=CE=B1=2Ee?= =?UTF-8?q?th?= <0xDADA@protonmail.com> Date: Wed, 26 Aug 2026 09:28:40 -0400 Subject: [PATCH 2/2] chore: don't add changelog --- CHANGELOG.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b464fc6..016add8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,6 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). -## [Unreleased] - -### Changed - -- `create-wallet --import` now writes the current chain tip to `.stealth-start-block` (same as a new wallet) so the first stealth scan does not walk historical announcements. Pass `--stealth-start-block` with no value for the previous Kohaku-schema floor, or `--stealth-start-block ` for an explicit floor. `balances --stealth-start-block` can still back-date below the file. - ## [0.0.4] — 2026-08-25 ### Added