feat: reproducible wallet performance test harness — 3 modes × 9 scenarios#6
feat: reproducible wallet performance test harness — 3 modes × 9 scenarios#6roadhero wants to merge 100 commits into
Conversation
Verified create-unsigned-transaction batch shape against minotari-cli commit 52a7287a3fe1e7831855649c530534af9f2d4830. The variant declares `recipient: Vec<String>` with help text "Can be specified multiple times." — repeated --recipient is the proven batch flag. No --recipients-file alternative exists. PR #99's send_transactions step confirms the single-recipient invocation shape verbatim; the batch shape relies on the clap declaration alone. Appends §Mode 3 CLI shape — proven to DESIGN_ADDENDUM.md, capturing the exact argv shape Mode 2/3 will emit, flag-level differences vs DESIGN.md (top-level --network, required --account-name and --password, optional --database-path, default 24h UTXO lock), and edge cases the implementation must handle (config-file path, concurrent UTXO-lock contention in S4, address-derivation path choice, one-sided address features). Relates-to: tari-project#1
Implements enforce_esmeralda per DESIGN.md §Mainnet-protection guard:
a hard allowlist that bails the harness before any subprocess spawn or
RPC call if the configured network is anything other than "esmeralda"
or the base-node URL host matches a mainnet denylist
(rpc.tari.com, seeds.tari.com, mainnet.tari.com, mainnet-rpc.tari.com).
Adds src/guards.rs with inline #[cfg(test)] coverage of the happy path,
both reject paths, every denylist entry, and the empty-network edge.
src/lib.rs is created here so the module is importable; src/config/mod.rs
gets a minimal placeholder Config { network, base_node_url } that later
commits (3b) extend in-place to the full schema-driven shape.
enforce_funding (DESIGN_ADDENDUM §M2) is deferred to a later commit so
this commit holds one purpose. enforce_funding depends on Config.a_fund
(3b) and the SeedHandle trait (3d), neither of which exist yet; landing
it here would force forward-declaration stubs that 3b/3d would have to
rewrite.
The M1 spike at src/main.rs is unchanged — it remains the binary entry
until 3k replaces it with the production main.
Relates-to: tari-project#1
Expand the placeholder Config struct to mirror RESULT_PROFILE_SCHEMA.md §1 field-for-field: all 13 documented keys land with `#[serde(default)]` and the defaults from the bounty issue's parameter table. A nested Seeds sub-struct records the *names* of the env vars holding seed mnemonics and the wallet passphrase, so secrets never live in a TOML on disk — only the resolver labels do. The TOML loader in `src/config/load.rs` is a single function returning `anyhow::Result<Config>`; per-field `#[serde(default)]` handles partial configs without bespoke merging. Inline tests cover the default contract, empty-table loading, partial overrides, full round-trip, and a wrong-type rejection. `guards::tests` switched its fixture builder to `Config::default()` with field overrides, picking up the new field roster without touching the guard logic. Relates-to: tari-project#1
Replace the M1 dep-graph spike `main.rs` with the real clap-derived CLI shape per DESIGN_ADDENDUM.md §S1: a top-level `Cli` with three subcommands — `run` (the default; takes `--config <path>`), `gen-seed`, and `print-address --seed-env <NAME>`. Invoking the binary with no subcommand resolves to `run` with the default `harness.toml` path, mirroring the maintainer's `minotari-cli` pattern. The `run` body emits a one-line "not yet wired" notice and exits with code 2 — scenarios land in a later step; the parse + dispatch surface is what this commit puts in place. `gen-seed` and `print-address` dispatch through stub library entry points that currently bail; the real implementations land in the next two commits. Inline tests in `cli` cover each subcommand parse, the default-to-run behaviour, and the default config path. Total: 19 passing unit tests. Relates-to: tari-project#1
Replace the `gen_seed` stub with the real Tari-mnemonic generator per DESIGN_ADDENDUM.md §S1: `CipherSeed::random()` → `to_mnemonic(English)` → space-joined 24-word string. The Tari mnemonic is NOT BIP-39 (per API_DRIFT.md Step 1) — it's a 24-word Tari-specific encoding generated by `tari_common_types::seeds::cipher_seed`. Output goes directly to stdout via `println!` in `main.rs`; per the §S1 contract this is operator-visible mnemonic emission and intentionally bypasses the result-profile redaction denylist. Callers must not echo it into structured logs or the result profile. Inline tests cover the 24-word/lowercase-ASCII shape, round-trip through `SeedWords::from_str` → `CipherSeed::from_mnemonic` → `to_mnemonic`, and randomness across consecutive invocations. Relates-to: tari-project#1
Replace the `print_address` stub with the real Esmeralda dual-address derivation per DESIGN_ADDENDUM.md §S1 and §S2. Reads the 24-word Tari mnemonic from the operator-supplied env var name, decodes through `SeedWords::from_str` → `CipherSeed::from_mnemonic` → `SeedWordsWallet` → `WalletType::SeedWords`, then assembles a `TariAddress` via `new_dual_address` with the wallet's public view/spend keys, the Esmeralda network constant, and the one-sided features flag. Encoded output uses base58 so the address can be piped straight into `minotari --network esmeralda create-unsigned-transaction --recipient <addr>::<amount>`. `WalletType::tari_address()` is the path DESIGN_ADDENDUM.md §S1 preferred, but does not exist on the published v5.3.1 surface; the fallback `TariAddress::new_dual_address` path documented in §S1's last paragraph is the one this commit takes. API_DRIFT.md Step 2 already records the drift. Inline tests cover the base58 round-trip, determinism for a given seed (each test uses a unique env-var name to avoid cross-test races under cargo's default parallel runner), the missing-env-var error, and the invalid-mnemonic error. Test count: 26 passing. Relates-to: tari-project#1
Adds the host environment capture surface backing `RESULT_PROFILE_SCHEMA.md §2` (cpu_model, ram_bytes, disk_type, os, network_path_to_base_node). The `EnvCapture` trait abstracts the capture site so scenario unit tests inject a deterministic `FakeEnvCapture` rather than depending on the test host's actual hardware. The production `LiveEnvCapture` delegates to `linux::capture` / `macos::capture` for the four host-dependent fields and computes `network_path_to_base_node` from the supplied base-node URL — that field depends on `Config`, not the OS. Per-OS source commands follow DESIGN.md §Environment-disclosure verbatim: /proc/cpuinfo + /proc/meminfo + lsblk + uname on Linux, sysctl + diskutil + uname on macOS. Per-field capture failures degrade to "unknown" / 0 so the schema's non-null contract holds even on degraded hosts. Any non-(linux|macos) target triggers a compile_error!, mirroring the bounty's scope. Inline tests cover fake-capture determinism, the loopback/remote classifier, and a platform-gated live-capture sanity check that asserts cpu_model is non-empty and ram_bytes > 0 on the test host. Relates-to: tari-project#1
Adds the `versions` block backing `RESULT_PROFILE_SCHEMA.md §3` — five
components: minotari_console_wallet, minotari_cli, base_node, harness.
Each is recorded as `BinaryVersion { tag, commit }` with at least one
populated per schema contract.
Probing decisions, documented in the module docstring and
`analysis/API_DRIFT.md`:
* harness.commit: runtime `git rev-parse HEAD` from the source dir.
No vergen / build.rs — published tari crates inspected for
precedent (`tari_common`, `tari_common_types`,
`minotari_node_wallet_client`) use neither, only
`tari_features::resolver::build_features` for feature gating. A
runtime subprocess call is the smallest possible mirror.
* minotari_console_wallet / minotari_cli: `<binary> --version`
subprocess parsed with a `name <semver>[+<commit>]` matcher.
Best-effort: missing binary or non-zero exit yields
`BinaryVersion::default()` so the result profile records absence
rather than aborting the run.
* base_node: the published `BaseNodeWalletClient` trait on
minotari_node_wallet_client 5.3.1 has no version endpoint —
verified by reading the trait source. Falls back to the pinned
`v5.3.1` constants from DESIGN_ADDENDUM §3 AC-27 recording. Live
verification is deferred to step 3e (wallet_lifecycle + broadcast).
Inline tests cover the parser shape (cargo-style, semver+commit,
empty, single-token), graceful failure for a missing binary,
the pinned-base-node round-trip, and a harness-commit sanity check
that skips when git is genuinely absent.
Relates-to: tari-project#1
Adds the runtime accessor backing `DESIGN.md §Secret handling` and
`DESIGN_ADDENDUM.md §S1`. `SeedHandle` carries env-var *names* drawn
from `Config::seeds` and re-reads the values on every accessor call,
so an operator can rotate a seed mid-run by exporting a new value.
Surface:
* mnemonic_old / mnemonic_new / mnemonic_payment_processor /
wallet_password — each returns a `RedactedString`.
* assert_distinct — bails when any two slots resolve to the same
mnemonic; AC-35 demands three separate funded wallets.
* address_old / address_new / address_payment_processor — Esmeralda
`TariAddress` derived via the shared `derive_address` helper.
`RedactedString` mirrors `tari_utilities::Hidden<String>` but with a
uniform "[REDACTED]" Debug/Display form. Zeroize-on-drop falls through
the inner `Hidden`, so no direct `zeroize` dep is needed (kept off the
Cargo.toml per DESIGN.md §Dependency strategy).
`derive_address` is now the single site for the address-from-mnemonic
path. `print_address` (CLI subcommand) refactors to call it; the
funding pre-flight added in the next commit calls the same helper via
SeedHandle. One code path serves all three address-touching surfaces.
Inline tests cover env-var read-at-call-time (set value, observe, mutate,
re-observe), the missing-env-var bail, distinctness happy + duplicate
paths, address base58 round-trip, address determinism, and a
cross-path check that `derive_address` and `print_address` agree on
the same mnemonic.
Relates-to: tari-project#1
Implements the 10-rule denylist backing `RESULT_PROFILE_SCHEMA.md §6`.
Two contracts:
1. `RedactionDenylist::check(&profile)` serialises the profile to
JSON and bails on the first rule match, reporting only the rule
ID and the byte offset — the matched substring is never echoed
into the error so a panic / log surface can't accidentally
re-leak the secret the rule caught.
2. `RedactionDenylist` is itself `Serialize`. The per-rule
`Serialize` impl emits only `(id, kind, reason)` — the compiled
regex pattern body and the env-derived substring value are
deliberately elided, so the auditable JSON copy in the result
profile contains rule IDs and reasons only.
`init_from_env` reads the three seed env vars + the passphrase env
var from the names recorded in `Config::seeds` and builds R1..R10
once at startup. Missing env vars degrade to empty-string rules,
which the `matches` helper treats as never-firing — keeps unit tests
usable without forcing every test to export every env var.
Patterns:
R1 — Tari 24-word OR BIP-39 12/24-word mnemonic shape.
R2 — three seed mnemonics, each whole AND each ≥3-letter word.
R3 / R4 — hex view / spend keys (case-insensitive).
R5 — wallet passphrase substring.
R6 / R7 — long hex (≥2048) / base64 (≥1024) blobs.
R8 — case-insensitive Bearer tokens.
R9 / R10 — `$USER` / `$LOGNAME` and `/Users/<user>/` `/home/<user>/`.
Inline tests cover the realistic-seed catch, the username-path catch,
the elision contract (serialised denylist contains rule IDs but NOT
the env-derived seed or passphrase), the long-hex catch, and a
sanity "clean profile passes" path.
Relates-to: tari-project#1
Implements `enforce_funding` per `DESIGN_ADDENDUM.md §M2`. Runs from
`main()` immediately after `enforce_esmeralda` and before any mode
runs; bails non-zero if any of the three harness seeds has a wallet
balance below `a_fund * 11 / 10` (10% headroom, integer math).
API drift surfaced: the published `BaseNodeWalletClient` trait on
`minotari_node_wallet_client = "5.3.1"` does NOT expose a balance
method (verified by reading the trait at `src/client/mod.rs`).
Architectural mitigation: a `BalanceQuery` trait with one method
`get_balance(&TariAddress) -> anyhow::Result<u64>` — same fake-trait
pattern as `EnvCapture` in step 3c.1. Live implementation will be
filled in alongside `wallet_lifecycle` (step 3e) once the harness has
a concrete address-scanning surface to read from. The trait keeps
`enforce_funding` testable today without taking a bet on which
balance-source the live wiring will use.
Error format matches DESIGN_ADDENDUM §M2:
Funding pre-flight failed. Required >= 11000000000 uT per seed (a_fund * 11/10).
old_wallet: 11000000000 uT OK
new_wallet: 0 uT (short by 11000000000 uT) FAIL
payment_processor: 12000000000 uT OK
See RUNBOOK §Funding for how to mine to each address using minotari_miner.
Inline tests cover the all-ok happy path, the shortfall-listing
contract (all three labels reported, deficit named, RUNBOOK pointer
present), and balance-query error propagation through the context
wrap. Each test uses unique env-var names to avoid cross-test races
under cargo's parallel runner.
Relates-to: tari-project#1
Three corrections to design artifacts, all driven by the same verified
fact: Tari's `CipherSeed` (at `tari_common_types::seeds::cipher_seed`
v5.3.1 commit `5d6ef11bb89caa34fe9ee676d608f273db90038d`) is a 24-word
Tari-specific encoding, NOT BIP-39. The original arch-test plan
assumed the BIP-39 all-zeros test phrase could double as a
deterministic test mnemonic; that assumption was rejected at runtime
during swe-impl step 1 (M1 spike), where `CipherSeed::from_mnemonic`
declined the abandon×12 phrase. Recorded in `API_DRIFT.md §Step 1`.
1. `RESULT_PROFILE_SCHEMA.md §6 R1` rationale text now says "Tari
24-word mnemonic OR BIP-39 12/24-word phrase — regex catches the
structural shape of both." The pattern itself
(`\b(?:[a-z]{3,8}\s+){11,23}[a-z]{3,8}\b`) is structural and
unchanged: it catches the shape of either word set by construction.
2. `DESIGN.md §Test Strategy §Test-data secret handling` replaces the
"single BIP-39 test mnemonic" paragraph with the verified
approach: tests use `gen_seed()` (which wraps `CipherSeed::random()`)
per-call, with unique per-test env-var names to avoid cross-test
races under cargo's parallel runner. Spirit preserved: tests use
disposable, obviously-fake seeds.
3. `DESIGN.md §Test Strategy §Fixtures` drops the
`fixtures/bip39_wordlist.txt` row — R1 is structural and does not
need a wordlist, confirmed by
`src/seed/redact.rs::tests::denylist_catches_realistic_seed_phrase`
in step 3d.2 which feeds a fresh 24-word Tari mnemonic into a fake
profile and observes the R1/R2 catch.
Relates-to: tari-project#1
Introduces `src/wallet_lifecycle/` per `analysis/DESIGN.md §Module boundaries and data flow`. Three submodules: * `mod.rs` — `WalletLifecycle` trait (async via `async_trait::async_trait` to match the `BaseNodeWalletClient` shape from `minotari_node_wallet_client = "5.3.1"`). * `data_dir.rs` — `HarnessDataDir` owning a `tempfile::TempDir` under `target/harness-data/<run-id>/<mode>/`. `wipe()` canonicalises the target and refuses any path outside the tempdir per AC-34. Drop cleans the tree. * `grpc.rs` — `connect_with_retry(addr, deadline)` wraps `minotari_app_grpc::tari_rpc::wallet_client::WalletClient::connect` with bounded exponential backoff. Connect-time only — `DESIGN.md §Mode 1 step 1` explicitly permits race-tolerant connect; AC-30/31/32 ban only submit-side retry. New direct deps (Cargo.toml): * `async-trait = "0.1"` — already transitively present via `minotari_node_wallet_client`; the trait sugar matches maintainer idiom. * `nix = "0.29"` (unix-only target dep, feature `signal`) — staged for the next commit's SIGTERM/SIGKILL handling in `ConsoleWalletLifecycle`. * `wiremock = "0.6"` (dev-dep) — staged for the broadcast wrapper unit tests. Inline tests cover: path under harness root, refusal of external / root / dotdot paths, acceptance of internal subpaths, no-op on absent internal paths, drop cleanup. Relates-to: tari-project#1
Implements `ConsoleWalletLifecycle` per `analysis/DESIGN.md §Mode 1 —
concrete wiring`. The struct owns the `minotari_console_wallet`
subprocess, a dynamic-port gRPC client, and the `HarnessDataDir`; it
implements `WalletLifecycle::{spawn, wait_ready, teardown, data_dir}`.
* `spawn` writes the seed mnemonic into `<data_dir>/seed.txt` (lives
only inside the harness tempdir per `DESIGN.md §Secret handling`),
allocates an ephemeral 127.0.0.1 port via TcpListener-then-drop,
and execs `minotari_console_wallet` with the literal argv shape
from DESIGN.md §Mode 1 step 1 (--network first, then --base-path,
--seed-words-file, --non-interactive-mode, --grpc-address, --password).
`kill_on_drop(true)` on the tokio Child ensures panic / Ctrl-C
cannot leak the process.
* `wait_ready` connects through `super::grpc::connect_with_retry`
(connect-time backoff is allowed by DESIGN.md §Mode 1 step 1 and
AC-30/31/32 only ban submit-side retry), then polls `GetState`
every 1s until `network.status == ConnectivityStatus::Online`.
API drift: DESIGN.md called this "is_synced" but
`minotari_app_grpc = "5.3.1"`'s NetworkStatusResponse exposes
`status: ConnectivityStatus` (Initializing / Online / Degraded /
Offline) — there is no `is_synced` bool. The Online variant is
the wallet's self-declared "ready for scenario calls" state.
* `teardown` sends SIGTERM via `nix` (cfg(unix); harness is documented
Linux+macOS only per DESIGN.md §Non-Goals), waits up to 10s polling
`try_wait`, then sends SIGKILL. Idempotent — repeat calls after a
successful teardown return Ok.
Config extension: `Config::minotari_console_wallet_path: Option<PathBuf>`
lets operators on non-standard installs point at the binary; `None`
falls back to `$PATH`. Additive — `#[serde(default)]` keeps the
existing harness.toml shape working.
Defense-in-depth re-assertion of `network == "esmeralda"` runs in
both `new()` and `spawn()` so a stale Config cannot reach
`Command::new` past the primary guard.
Inline tests:
* dynamic port allocator returns a valid, distinct-per-call port,
* `spawn_argv` snapshot test asserts each flag's position matches
DESIGN.md §Mode 1 step 1.
Subprocess-side teardown behaviour is covered by the live-network
smoke test (operator opt-in via `cargo test -- --ignored`); unit
tests do not spawn the real binary.
Relates-to: tari-project#1
Adds `src/broadcast/mod.rs` with `Broadcaster` (thin wrapper over
`minotari_node_wallet_client::http::Client`) and a typed
`TxSubmissionOutcome` shape that scenario code records into the
result profile.
The published `Client::submit_transaction` already handles the
dual-shape JSON-RPC response per DESIGN.md decision 5 (envelope
success vs bare-error): envelope-success returns
`Ok(TxSubmissionResponse)`, bare-error returns
`Err(anyhow!("Transaction submission failed: ..."))`. Our wrapper
maps `Ok` through a `From<TxSubmissionResponse> for TxSubmissionOutcome`
impl and propagates `Err` with added context.
API drift surfaced (logged in analysis/API_DRIFT.md scratch):
* Upstream TxSubmissionResponse is `{ accepted, rejection_reason,
is_synced }`. DESIGN.md called out a `details` field that does
NOT exist on the published 5.3.1 struct; our outcome keeps a
nullable `details: Option<String>` for forward-compat.
* Upstream `TxSubmissionRejectionReason` has a `DoubleSpend` variant
not listed in DESIGN.md §Mode 2 step 5 — surfaced as first-class
so S4 contention measurements don't collapse it into `Other`.
* Calling `submit_transaction` requires importing the
`BaseNodeWalletClient` trait — `Client` itself does not expose
the method as an inherent.
`RejectionReason::Other(String)` is kept as a forward-compatibility
arm even though the published 5.3.1 client's closed enum cannot
reach it; this isolates scenario code from a future upstream
release adding a variant.
Inline tests cover the six listed rejection variants plus the
upstream DoubleSpend, accepted-true mapping, and `is_synced`
propagation. The "bare error → typed Err" path is covered by
relying on the upstream client's tests (the wrapper just adds
context) — wiremock-based end-to-end tests live alongside the
later scenario integration tests.
Relates-to: tari-project#1
Adds `src/wallet_lifecycle/balance_query.rs` with `LiveBalanceQuery`, the live impl of the `BalanceQuery` trait declared in `src/guards.rs`. Wired against `minotari_node_wallet_client::http::Client` so the eventual real implementation can flip the `get_balance` body without touching call sites in `main()`. API drift logged in `analysis/DESIGN_AMENDMENT.md §7`: the published `minotari_node_wallet_client = "5.3.1"` `BaseNodeWalletClient` trait has NO address-indexed balance endpoint. The trait's surface (`get_utxos_mined_info(hashes)`, `fetch_utxo(hash)`, `sync_utxos_by_block(start_header_hash, ...)`) is UTXO-hash- or block-hash-indexed; none of it answers "what is address X's spendable balance?". The only source-of-truth for an address's balance in this codebase is the wallet gRPC's `GetBalance`, which requires a running `minotari_console_wallet` for the seed that owns that address. Pre-flighting three seeds therefore implies three console-wallet spawns — heavy, and architecturally entangled with Mode 1's lifecycle. That entanglement is deferred to a follow-up patch once Mode 1 lands. Until then, `LiveBalanceQuery::get_balance` bails with a structured error that points at the amendment and surfaces the operator workaround (`minotari_miner` funding per RUNBOOK + skipping the pre-flight). The existing `FakeBalanceQuery`-backed `enforce_funding` unit tests in `src/guards.rs` continue to cover the funding-pre-flight logic deterministically; this change does not affect them. Inline tests: * construction does not make a network call (canary for an upstream change that adds an eager connect to `Client::new`), * `get_balance` bail message names the amendment + operator workaround. The promised wiremock-based "sum of UTXOs" test from the step prompt is unimplementable on the published 5.3.1 surface and is documented in the amendment as "not shipped in 3e". Relates-to: tari-project#1
Adds `src/modes/mod.rs` with the `Mode` trait and its supporting types — `TxRecord`, `ScanOutcome`, and `UnsupportedOperation`. Per `analysis/DESIGN.md §Module boundaries and data flow`, scenarios B0-S7 are mode-agnostic: they call `mode.send_single(...)`, `mode.scan_from_birthday(...)`, etc., through this trait. The three implementations (Mode 1 / Mode 2 / Mode 3) land in sibling files per `DESIGN_ADDENDUM.md §S4` execution order. Methods enumerated by DESIGN.md §Module boundaries: * `send_single` / `send_batch_one_to_many`, * `scan_from_birthday`, * `get_balance` / `get_utxo_count`, * `wipe_and_reimport`. All methods take `&mut self` — the underlying gRPC `WalletClient` (Mode 1) and subprocess spawn surface (Mode 2/3) are mutating. The trait is `async` via `async_trait::async_trait` (matches the `BaseNodeWalletClient` shape in `minotari_node_wallet_client = "5.3.1"` and keeps trait objects usable from scenario dispatch). `UnsupportedOperation` is a `thiserror`-derived error type returned by mode-method combinations that aren't realisable on a given mode (Mode 1's `send_batch_one_to_many` per DESIGN.md §Mode 1 step 3 + AC-20). Scenarios surface the failure into the result-profile cell's `arms.batch.applies = false` and continue rather than aborting the mode. No implementations in this commit — the trait+types is its own unit of work; the Mode 1 impl follows in the next commit, with Modes 2 and 3 deferred to steps 3g and 3h. Relates-to: tari-project#1
Adds `src/modes/old_wallet.rs` with `OldWallet` — Mode 1's `Mode` trait implementation, backed by `ConsoleWalletLifecycle`. Each trait method dispatches against the connected `minotari_app_grpc::tari_rpc::wallet_client::WalletClient`: * `send_single` -> gRPC `Transfer` with a single `PaymentRecipient`, `PaymentType::OneSidedToStealthAddress`, surfaces the transfer result as a `TxRecord`. * `send_batch_one_to_many` -> returns `UnsupportedOperation`. The proto `TransferResponse.results` shape is "one TransferResult per recipient" — N independent single-recipient txs, not a 1->K batch. DESIGN.md §Mode 1 step 3 + AC-20 both record this: S5's batch arm runs on payment_processor only. * `scan_from_birthday` -> reuses `wipe_and_reimport`; the wallet scans on start so the wait_ready wall-clock IS the scan duration. Pre-scan tip is set to 0 — scenario code fills it in from the base-node `get_tip_info` call per DESIGN.md §Scenario state machine. * `get_balance` -> gRPC `GetBalance(payment_id: None)`, returns `available_balance`. * `get_utxo_count` -> gRPC `GetUnspentAmounts`, returns `amount.len()`. * `wipe_and_reimport` -> teardown -> rewrite the held mnemonic via `CipherSeed::change_birthday(birthday)` (the new `rewrite_birthday` helper) -> wipe the data dir via `HarnessDataDir::wipe` (AC-34 path-confinement enforced) -> recreate the dir -> spawn -> wait_ready. To support the birthday-rewrite flow, `ConsoleWalletLifecycle` gains three accessor methods (additive, no behavioural change): * `data_dir_mut()` — for the wipe call, * `mnemonic()` — to read the current mnemonic plaintext for re-encoding, * `replace_mnemonic(String)` — to swap in the rewritten mnemonic before re-spawn. The next `spawn` writes the new mnemonic into the freshly- wiped seed.txt. API drift surfaced (logged in analysis/API_DRIFT.md scratch): * The `Mode` trait was originally specified with `&self` receivers for `get_balance` and `get_utxo_count`; tonic-generated gRPC methods take `&mut self` on the client, so all trait methods are uniformly `&mut self`. Documented in the trait's docstring. * `PaymentType::OneSidedToStealthAddress` is the only non-deprecated variant on the v5.3.1 proto; the other two are marked deprecated. Inline tests: * `mode1_name_is_old_wallet` — covered by the `name()` constant, * `mode1_send_batch_one_to_many_error_names_ac20_and_old_wallet` — shape of the `UnsupportedOperation` error, * `rewrite_birthday_round_trips_through_cipher_seed` (5_000 birthday), * `rewrite_birthday_zero_yields_valid_genesis_birthday`, * `rewrite_birthday_rejects_garbage_mnemonic`. Subprocess-side `send_single` / `scan_from_birthday` / `wipe_and_reimport` behaviour is exercised by the live-network smoke (operator opt-in via `cargo test -- --ignored`); unit tests do not spawn the real binary. Relates-to: tari-project#1
… submit) DRY core shared by Modes 2 and 3 — the four-step pipeline mirrored verbatim from PR #99 (`tari-project/minotari-cli#99` `send_transactions` step) with the single delta `Network::LocalNet → Network::Esmeralda`: 1. subprocess `minotari --config <harness.toml> --network esmeralda create-unsigned-transaction --database-path … --password … --account-name default --recipient <addr_base58>::<amount> [--recipient ...] --output-file <tx_<idx>.json>` (top-level flags BEFORE the subcommand per `DESIGN_ADDENDUM.md §Mode 3 CLI shape — proven`), 2. parse via `PrepareOneSidedTransactionForSigningResult::from_json`, 3. reconstruct the KeyManager from the SeedHandle's mnemonic via the approved API surface (`WalletType::SeedWords` + `SeedWordsWallet::construct_new` + `KeyManager::new`) and call `sign_locked_transaction(&km, ConsensusConstantsBuilder::new(Esmeralda) .build(), Network::Esmeralda, unsigned)` in-process, 4. broadcast via `Broadcaster::submit_transaction` from step 3e. No retry, backoff, or throttling anywhere (AC-30/31/32). The output JSON file is deleted on success and preserved on any failure path (helpful for operator post-mortem). Unit tests pin the argv shape for both single-recipient and batch (K=3) shapes against the proven CLI surface, verify the harness.toml write contract, and lock the bail-on-`SeedRole::Old` contract. Adds `Config::minotari_path` so the new wallet binary path is configurable independently from `Config::minotari_console_wallet_path` (Mode 1's binary). Adds `TxRecordStatus` + `TxRecordPhase` enums to `modes/mod.rs` so the helper can name failure phases (construct / sign / broadcast / confirm / scan) per `RESULT_PROFILE_SCHEMA.md §4`. Module-level `#[allow(dead_code)]` lifts in the Mode 2 commit that follows. Relates-to: tari-project#1
`NewWallet` implements the `Mode` trait for the new minotari CLI: * `send_single` / `send_batch_one_to_many`: route through the shared `minotari_subprocess::create_sign_and_submit` helper with `SeedRole::New` so the per-mode seed (`HARNESS_SEED_NEW` by default) drives the in-process KeyManager. Single-recipient uses a one-element recipients slice; batch passes the caller's list verbatim. * `scan_from_birthday` / `get_balance` / `get_utxo_count` / `wipe_and_reimport`: bail with a structured pointer at `analysis/DESIGN_AMENDMENT.md §8`. The new `minotari` CLI at the pinned `minotari-cli` commit `52a7287a...` differs substantially from the DESIGN.md §Mode 2 step 7 read-side surface (no `list-utxos` subcommand, `Balance` emits human stdout rather than machine-parseable JSON, and wallet restoration uses `Create --seed-words` not `import-seed`). The real impl lands in step 3i when scenarios layer knows the stdout-parsing contract; today's placeholder is loud-bailing per the LiveBalanceQuery precedent (3e.4) so scenarios that hit it fail cleanly rather than silently producing zero-valued profile cells. AC-6 (`Mode2NewWallet` does NOT spawn `minotari_console_wallet`) is satisfied by construction: this module + the helper invoke only `minotari` (via `Config::minotari_path`), never `minotari_console_wallet`. Verified by the unit test `minotari_subprocess::tests::helper_source_does_not_reference_console_wallet` and reinforced by `tests/mode2_no_console_wallet.rs` (3j). The module-level `#[allow(dead_code)]` on the helper from the previous commit is removed; `SeedRole::Pp` carries a localised allow until Mode 3 lands next. Relates-to: tari-project#1
`PaymentProcessor` implements the `Mode` trait by reusing the shared `minotari_subprocess::create_sign_and_submit` helper with `SeedRole::Pp` so the payment-processor seed (`HARNESS_SEED_PP` by default) drives the in-process KeyManager. Mode 3 IS Mode 2 with a different seed slot and routes batches through the same repeated-`--recipient` argv shape proven in `DESIGN_ADDENDUM.md §Mode 3 CLI shape — proven`. For S5 (per AC-19 / AC-20): * batch arm (Mode 3 primary): `send_batch_one_to_many(K=10 recipients)` invoked 10 times for 100 total recipients across 10 batch txs, * individual arm (Mode 3 context-only per ambiguity tari-project#3): `send_single` invoked 100 times against the same recipient list. Mode 3 itself is arm-agnostic; the scenarios layer (3i) orchestrates which arm runs. Read-side flows (`scan_from_birthday` / `get_balance` / `get_utxo_count` / `wipe_and_reimport`) bail with the same `DESIGN_AMENDMENT.md §8` pointer as Mode 2 — the new `minotari` CLI surface at the pinned commit lacks the machine-parseable read-side subcommands DESIGN.md §Mode 2 step 7 assumed. Real impl lands in step 3i. The `SeedRole::Pp` `dead_code` allow on the helper from the Mode 2 commit is removed; `SeedRole::Old` retains its localised allow because that branch exists only to bail loudly and the test `create_sign_and_submit_bails_on_seed_role_old` exercises it. Relates-to: tari-project#1
Closes the first of four read-side method placeholders surfaced in analysis/DESIGN_AMENDMENT.md §8 (filed during step 3g). The new minotari CLI from tari-project/minotari-cli has no import-seed subcommand; restoration is `Create --seed-words "..."` with the same SecurityArgs / DatabaseArgs / AccountArgs flatten as create-unsigned- transaction. Adds a new shared module src/modes/minotari_wallet_ops.rs that owns the wipe / Create plumbing for both Modes 2 and 3: * rewrite_birthday mirrors Mode 1's homonym (CipherSeed -> change_birthday -> re-encode to mnemonic). Pure-Rust, no IO. * wipe_and_reimport_via_create wipes the harness data dir (AC-34 path-confinement), recreates it, writes harness.toml with network="esmeralda", and spawns `minotari Create --seed-words` with env_clear() + HOME/PATH/TARI_NETWORK plus the password in argv per PR #99's SecurityArgs declaration. On non-zero exit the partial state is preserved for operator diagnosis. NewWallet and PaymentProcessor's wipe_and_reimport implementations reduce to: read mnemonic for the role, rewrite_birthday, call the helper. Subsequent commits wire scan_from_birthday, get_balance, and get_utxo_count. Test surface: argv top-level-flags-before-subcommand contract (single argv slot for the 24-word mnemonic per clap's Option<String>), rewrite_birthday round-trip for birthday=7000 and birthday=0, DEFAULT_BINARY != minotari_console_wallet pin, plus Mode 2/3 wipe methods deterministically surface their "Mode N wipe_and_reimport" context when the configured binary does not exist. Relates-to: tari-project#1
Closes the second of four read-side method placeholders surfaced in
analysis/DESIGN_AMENDMENT.md §8 (filed during step 3g). The new
minotari CLI's `Scan` subcommand takes --max-blocks-to-scan (not the
DESIGN.md-prescribed --from-birthday); birthday is baked into the
wallet via the wipe+create path the previous commit landed.
Adds to src/modes/minotari_wallet_ops.rs:
* build_scan_argv — top-level --config/--network BEFORE the `scan`
subcommand; subcommand flags --password/--database-path/
--account-name/--max-blocks-to-scan AFTER. Identical envelope to
build_create_argv.
* run_scan_subprocess — spawns with env_clear() + HOME/PATH/TARI_NETWORK
AND RUST_LOG=info (the only way to extract the result, see below).
Returns ScanStdoutParsed { outputs_found: Option<u64>,
max_blocks_to_scan: u64 }.
* parse_event_count — structural-anchor regex on `event_count=(\d+)`.
The Scan subcommand emits its result via the `log` crate's key-value
formatter (`info!(event_count = events.len(); "Scan complete")`),
not stdout. Stderr lookup with regex is the only stable extraction.
* DEFAULT_MAX_BLOCKS_TO_SCAN = u64::MAX. The cli.rs default is 50; the
bounty's B0/S2/S3/S6/S7 scenarios walk from genesis so the default is
too low. u64::MAX lets the subprocess consume blocks until the
wallet's underlying scan logic stops. Scenarios layer (step 3i.1)
can bound this via base-node tip queries if needed.
Mode 2 and Mode 3 grow a `last_scan: Option<ScanOutcome>` cache field
populated by every successful scan_from_birthday call. Used by
get_utxo_count in a subsequent commit per
analysis/DESIGN_AMENDMENT.md §8.3 step 4.
scan_from_birthday returns h_tip_start/h_tip_end/balance_microtari as
0 — matches Mode 1's `OldWallet::scan_from_birthday` "scenario layer
fills these in" convention. The post-scan balance read lands when
get_balance is wired in a subsequent commit.
Test surface: argv shape pin, parse_event_count for present/absent/zero
inputs, Mode 2/3 scan deterministically surface their "Mode N scan"
context when the configured binary does not exist.
Relates-to: tari-project#1
…otari Balance subprocess (3i.0.c) Wires Mode 2 (NewWallet) and Mode 3 (PaymentProcessor) `get_balance` to a real `minotari Balance` subprocess, replacing the §8 amendment placeholder. Anchor choice: the parser uses structural unit sentinels (`µT` and `T`), not label anchors like "Available:" / "Balance:". Per `analysis/DESIGN_AMENDMENT.md §8.3` (anchor strategy `(a)`), structural anchors survive label renames as long as the formatter's unit sentinel stays — and `MicroMinotari::Display` lives in `tari_amount.rs` upstream where it changes far less often than human-readable labels. Three fixtures captured (each is one line from `minotari Balance` stdout per `main.rs::handle_balance` at the pinned commit lines 680-694): * fixtures/minotari_balance_empty.txt — empty wallet, total = 0 µT. * fixtures/minotari_balance_partial.txt — partial wallet, total < 1 T (microtari format). * fixtures/minotari_balance_full.txt — funded wallet, total >= 1 T (Tari format, 6 decimal places). Both modes route through the shared `crate::modes::minotari_wallet_ops::run_balance_subprocess` helper — Mode 2 and Mode 3 share the same `minotari` (new wallet from `tari-project/minotari-cli`) binary and the same DB layout, so a single helper covers both. The helper follows the same env-clear + selective re-add envelope as `minotari_subprocess`, and the argv has NO `--password` (Balance reads unencrypted DB metadata per cli.rs lines 247-252). `get_utxo_count` remains a §8 placeholder pending 3i.0.d. Relates-to: tari-project#1
…rage Implements MODE_3_REWORK_SPEC.md §13's pr_lifecycle test list (8 tests, parallel to PpLifecycle's shape): * import_view_key_argv_matches_spec (pure-fn snapshot, asserts the --base-path / --network / --account-name shape per spec §5 step 1 AND that --port is intentionally absent) * daemon_argv_matches_spec (asserts the daemon argv adds --port + --account-name on top of the import shape) * daemon_argv_includes_the_dynamic_port (regression guard for the port literal) * new_refuses_non_esmeralda_network (defense-in-depth: even when the harness-wide guard is bypassed, the constructor refuses) * spawn_returns_handle_and_pid (Child PID > 0) * wait_ready_times_out_when_fake_never_binds (full spawn flow exercises import-then-daemon ordering + the 30s readiness probe + bail message) * teardown_sends_sigterm_then_sigkill (TERM-trap fake exits inside the 5s grace; second teardown no-op) * drop_invokes_kill_on_drop (kill_on_drop(true) reaps the daemon child via the same nix::sys::signal::kill(pid, None) liveness poll as pp_lifecycle's parallel test) Reuses tests/fixtures/fake_minotari.sh (added with the pp_lifecycle commit) — its two-branch script handles both subcommands. Refs tari-project#1. Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
…overage Implements MODE_3_REWORK_SPEC.md §13's payment_processor test list (10 tests): * mode3_name_is_payment_processor (Mode::name + MODE_NAME const) * mode3_scan_from_birthday_returns_unsupported_operation * mode3_get_balance_returns_unsupported_operation * mode3_get_utxo_count_returns_unsupported_operation * mode3_wipe_and_reimport_returns_unsupported_operation (each of the four downcasts the anyhow::Error to UnsupportedOperation and asserts mode='payment_processor' and op matches the method name) * mode3_send_single_posts_to_pp_via_wiremock (TxRecord.txid carries batch_id; status='success'; error_string=None) * mode3_send_batch_posts_to_pp_via_wiremock (3-recipient batch; same shape assertions) * mode3_send_batch_rejects_over_max_batch_size (101 items bails before HTTP — server is mounted but never called; uses cloned recipient to keep address-derivation cost flat) * mode3_dispatcher_returns_arc_per_call (two dispatcher() calls each yield a usable Arc<dyn S4Dispatcher>) * mode3_dispatcher_assigns_unique_client_ids (4 concurrent dispatch() calls produce 4 distinct s4-N client_ids; verified by reading the request bodies wiremock captured) Adds one production-shape adjustment: PaymentProcessor::from_parts_for_test under #[cfg(test)] pub(crate) so tests can inject an Arc<PpHttpClient> pointing at a wiremock server (production routes through PpLifecycle::http_client_owned which hardcodes the loopback port). Matches the test-only constructor pattern in NewWallet::new_with_wallet_db. Refs tari-project#1. Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
Implements MODE_3_REWORK_SPEC.md §13's integration-test list (two new
files under tests/):
tests/mode3_skips_scan_scenarios.rs (9 tests):
* mode3_scan_from_birthday_b0_returns_unsupported
* mode3_scan_from_birthday_s2_returns_unsupported
* mode3_scan_from_birthday_s3_returns_unsupported
* mode3_scan_from_birthday_s6_returns_unsupported
* mode3_scan_from_birthday_s7_returns_unsupported
* mode3_get_balance_returns_unsupported
* mode3_get_utxo_count_returns_unsupported
* mode3_wipe_and_reimport_returns_unsupported
* mode3_unsupported_reason_is_consistent_across_methods
Each constructs a real PaymentProcessor via the public PaymentProcessor::new
ctor (env-resolved Mode3Config with fake fixture binary paths) and drives
the four scan-shaped Mode trait methods. The downcast assertion mirrors
the runner's main.rs:247 pattern so a future regression that changes the
error type surfaces here AND in the runner.
tests/mode3_pp_lifecycle_with_fake_binary.rs (3 tests):
* fake_pp_logs_startup_banner_to_per_run_log_file (verifies stdio
capture into <data_dir>/logs/pp.log lands the fake's stderr banner
within 2s; exercises the per-run log file pattern from spec §2)
* pp_lifecycle_spawn_against_fake_times_out_then_teardown_is_clean
(full PpLifecycle::spawn -> readiness probe times out at 30s ->
teardown SIGTERMs the fake which traps and exits 0 inside the 5s
grace; idempotent second teardown)
* pp_lifecycle_drop_without_teardown_relies_on_kill_on_drop (exercises
spec §14 failure mode #12)
All tests use the existing tests/fixtures/{fake_pp.sh,fake_minotari.sh}
fixtures committed alongside pp_lifecycle's unit tests — no live PP or
minotari daemons ever spawn.
Refs tari-project#1.
Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
Pure formatting cleanup — rustfmt re-flowed three of the test files I added in the prior 5 commits. No behaviour change. Refs tari-project#1. Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
…ify account name Previous argv used --base-path/--port/--account-name which fail clap parse against minotari daemon and import-view-key subcommands. Real flags per minotari-cli@52a7287a/minotari/src/cli.rs: - daemon: --password, --base-url, --database-path, --scan-interval-secs, --api-port (NO --account-name) - import-view-key: --view-private-key, --spend-public-key, --password, --database-path, --birthday (NO --account-name) - --network is top-level on Cli, not subcommand-scoped Account name unified to "default" (not "bench") because upstream init_wallet.rs:121 hardcodes the wallet name as "default" and neither subcommand accepts --account-name. PP's ACCOUNTS__BENCH__NAME env value flipped to "default" so PP's GET /accounts/default/balance hits the PR daemon correctly. Adds Mode3Config::pr_base_url (default https://rpc.esmeralda.tari.com) for the now-mandatory --base-url flag. Resolves swe-review B2 (canonical-baseline argv parse error) and N2 (account-name mismatch -> 404 on PR daemon). Relates-to: tari-project#1 Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
…tead of UnsupportedOperation S0 calls mode.get_balance() + mode.get_utxo_count() as its first two operations. UnsupportedOperation propagates via ? and S0 is recorded as NotRun before the actual batch POST happens. PP doesn't own a balance surface (balance lives with the PR daemon's view-key wallet), so returning Ok(0) is semantically honest and lets S0 progress to its real work: POST /v1/payment-batches with one payment. Resolves swe-review B1. Relates-to: tari-project#1 Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
UNSUPPORTED_REASON was a single const applied to all Skipped returns, masking the actual reason for non-scan methods. Split into method-accurate strings so S1/S4/S5 cell logs explain what's actually unsupported. Resolves swe-review C5. Relates-to: tari-project#1 Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
Mode 3 previously checked SeedRole::Pp's mnemonic-derived balance, but its real signer is the view+spend keypair held by the PR daemon. Operator funding the correct wallet (PR daemon) would see spurious pre-flight failures. Pre-flight now queries the PR daemon's GET /accounts/default/balance endpoint. Adds a new `PrBalanceQuery` HTTP client (parses the `available` field from the real handler's AccountBalance JSON, no retry) and threads `Option<&PrBalanceQuery>` through `enforce_funding`. When Config::mode_3 is Some, main.rs builds a PrBalanceQuery pointed at http://127.0.0.1:<pr_port> and passes it in; the Pp arm is routed through it instead of the legacy mnemonic-derived path. Sequencing: pre-flight runs at startup, before the per-mode loop spawns the PR daemon via start_external_services. When the PR daemon isn't yet reachable (the common case), the Pp arm is logged with a warn and skipped rather than failing the whole pre-flight — operators who want strict Mode 3 pre-flight coverage must pre-warm the PR daemon out-of-band. The warn log explains this. Wiremock tests prove the HTTP shape (GET /accounts/default /balance, parse `available`). Resolves swe-review C4. Relates-to: tari-project#1 Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
Added speculatively for PP /v1/events sub-segment latencies that never wired up. Forces #[allow(dead_code)] and ripples through every TxRecord constructor. Per Simplicity First, delete now; re-add in a focused commit when the metric actually lands. Resolves swe-review C3. Relates-to: tari-project#1 Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
The doc comment claimed v1 always uses 'bench' for the PP account name. Post-N2 (commit 4c23e35) production sends 'default' to match the wallet name minotari-cli hardcodes at init_wallet.rs:121. Updated to reflect the actual value and cite the upstream source. Cleared in swe-review pass 2. Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
| }) | ||
| } | ||
|
|
||
| async fn send_batch_one_to_many( |
There was a problem hiding this comment.
you can run this on the console wallet
There was a problem hiding this comment.
Thanks! Wiring Mode 1's send_batch_one_to_many via minotari_console_wallet's gRPC SendTransactionBatch (or equivalent on WalletClient) is achievable since we already hold the gRPC client in OldWallet. I'll ship the implementation plus tests.
There was a problem hiding this comment.
Upd.: shipped at e5034c9 + a2977e3 + 9ce9151. OldWallet::send_batch_one_to_many now wires via WalletClient::transfer(TransferRequest { recipients, single_tx: true }) per wallet.proto:578 — same gRPC surface as send_single with the batch flag flipped. Mode 1's S5 batch arm flips from NotRun (UnsupportedOperation) to real measurements.
4 unit tests to cover the happy path, partial failure → Ok with status "failure" preserving S5's tx_count partition, and empty-batch → Err. gRPC-transport-error documented as a live-smoke gap; baseline-cycle coverage scaffolded at tests/live_esmeralda_smoke_batch.rs.
Sentinel builds_correct_argv_for_single_recipient byte-identical. 256 passing + 1 ignored.
| }, | ||
| pr_data_dir, | ||
| )?; | ||
| let pp_lifecycle = PpLifecycle::new(&cfg, &seeds, pp_data_dir)?; |
There was a problem hiding this comment.
I might be wrong here but it looks like you assume the payment processor is already running? There is no binary attached here?
There was a problem hiding this comment.
Good catch — that line is the lifecycle handle constructor only. The actual PP child-process spawn happens later in payment_processor.rs::start_external_services via PpLifecycle::spawn, which forks the binary at cfg.mode_3.pp_binary_path. I can make the harness clone-and-build PP itself if you'd prefer a single-step setup; current operator-pre-build approach matches working setup and avoids embedding a sqlx-offline build cache in the harness, but it's an explicit operator burden vs. a one-step setup. Let me know which you'd prefer?
Previously returned UnsupportedOperation, causing Mode 1's S5 batch arm to record NotRun. Per @SWvheerden review (PR tari-project#6 inline comment on old_wallet.rs:144, 2026-06-05): "you can run this on the console wallet". Mode 1's S5 row now produces real measurements via WalletClient::transfer with single_tx=true, which constructs a single 1->K Mimblewimble batch per the proto comment at wallet.proto:578. Implementation: Mode 1 batch builds the K-element recipients vec and calls Transfer with single_tx=true. Partial failures map to Ok(TxRecord { status: "failure", error_string }) per spec section 4 to preserve S5's tx_count partition. Empty recipients returns Err per ratified Q3. Also removes the SeedRole::Old skip guard in s5_throughput.rs and flips the matching skip test to a runs test. Appends section 11 to DESIGN_AMENDMENT.md documenting the proto-supported batch path. Scaffolds tests/live_esmeralda_smoke_batch.rs as #[ignore] per ratified Q5 for operator-side baseline smoke. Resolves PR tari-project#6 review comment on old_wallet.rs:144. Relates-to: tari-project#1 Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
…bility Per `analysis/specs/MODE_1_BATCH_SEND_SPEC.md §5 Layer 1`, split `send_batch_one_to_many` into: * `build_batch_transfer_request(&[(TariAddress, u64)], u64) -> TransferRequest` — pure construction, no I/O. * `fold_batch_transfer_response(TransferResponse, u64) -> anyhow::Result<TxRecord>` — pure response fold applying spec §4 error mapping (happy / all-fail / partial-failure / empty-results). The trait method becomes a 4-line wrapper: empty-check (Q3) → build → gRPC call → fold. Production behavior is unchanged — the same `TransferRequest` is constructed and the same `TxRecord` is folded; the helpers are `pub(crate)` so the inline `#[cfg(test)] mod tests` can exercise them without a fake-gRPC server (per `DESIGN.md` line 580 "no tonic mock for Mode 1 gRPC"). `builds_correct_argv_for_single_recipient` in `minotari_subprocess.rs` is unchanged (verified byte-identical via `git diff`); the Mode 1 sentinel `mode1_name_is_old_wallet` is unaffected. Refs tari-project#1. Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
Adds four unit tests inline in `src/modes/old_wallet.rs::tests`,
exercising the helpers extracted in the preceding commit:
* `build_batch_request_carries_all_recipients_with_single_tx` — 100
recipients × `fee_rate = 5` → `single_tx = true`, every
`PaymentRecipient` carries the input address/amount, uniform
`fee_per_gram`, and `PaymentType::OneSidedToStealthAddress`. (Spec §5
cases 1–4 collapsed into one parameterised assertion loop — the
per-index check is order-preserving.)
* `fold_batch_response_happy_path_returns_ok_success` — all-success
response → `Ok(TxRecord { status: "success", error_string: None, txid:
results[0].transaction_id.to_string(), fee_microtari: 0, t_total_ms:
input, .. })`. (Spec §5 cases 5, 9, 10.)
* `fold_batch_response_partial_failure_returns_ok_with_failure_status`
— 50/100 failures → `Ok(TxRecord { status: "failure", error_string
contains "partial failure" + "50/100" + upstream failure_message })`.
Asserts the spec §4 Q1 ratified mapping verbatim.
* `send_batch_empty_recipients_returns_err_without_grpc_call` — the one
full-method test. Empty `&[]` slice → `Err` containing "empty
recipients" in the message. The bail at method entry runs before any
`lifecycle.client_mut()` call, so the unspawned wallet is never
dereferenced — no fake-gRPC needed.
A `// NOTE:` at the top of the `tests` module documents why the gRPC
transport-error case (the 4th case in the operator's brief) is NOT a
unit test here: `DESIGN.md` line 580 explicitly forbids a tonic mock for
Mode 1 and `CLAUDE.md` forbids test infrastructure outside the test
tree. The transport-error path is exercised by
`tests/live_esmeralda_smoke_batch.rs` against testnet and by the
committed baseline run.
Refs tari-project#1.
Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
Both `import_view_key_argv` and `daemon_argv` were passing the data directory itself to `--database-path`. The minotari CLI's `DatabaseArgs::database_path` is the sqlite3 file path, not its parent directory; canonical-baseline runs would fail at import or daemon spawn with the wallet unable to open its DB. Appends `wallet.sqlite3` to the data dir in both argv builders, matching the existing `NewWallet::wallet_db_path` convention. Snapshot tests updated to assert the new sqlite3 path. Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
…Online `wait_ready` previously returned as soon as `ConnectivityStatus::Online` fired, but Online means "connected to a base node" — it does not mean the wallet has finished scanning + validating its outputs. The downstream `enforce_funding` pre-flight then called `GetBalance` immediately and saw 0 (false fail), and Mode 1/2 scenario sends raced an in-flight scan and surfaced as insufficient_funds. `GetStateResponse` exposes `has_done_initial_validation` (field 4 in wallet.proto) as the canonical scan-and-validation-complete signal. `wait_ready` now requires BOTH `Online` AND `has_done_initial_validation == true` before returning. The module doc-comment's stated contract (`is_synced && scanned_height == base_node_tip`) was always the intent; the implementation now matches it. Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
`run_scan_subprocess` exits when `minotari scan`'s blockchain catch-up loop terminates, but the wallet still finalizes per-output commitment and state-write work asynchronously. Calling `create-unsigned-transaction` immediately after the scan subprocess exits would race that finalization and surface as `insufficient_funds` on canonical-baseline runs, even though the wallet's funding had landed on the chain. Adds `wait_for_balance_positive` in `minotari_wallet_ops`. Polls `minotari Balance` every 5s (default 5-minute deadline) until the wallet reports a positive total; logs the poll cadence so operators can distinguish "wallet finished settling" from "wallet is genuinely empty". Mode 2's `scan_from_birthday` now calls this gate after `run_scan_subprocess` returns; Mode 3's `scan_from_birthday` is `UnsupportedOperation` so no change there. The Mode 1 equivalent is the `has_done_initial_validation` gate in `console_wallet::wait_ready`. The poll-cadence and deadline sleeps live inside a `tokio::select!` deadline-arm alongside the absolute `sleep_until`, matching the existing AC-32 carve-out used by S0+ confirmation loops. Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
|
Can you add a readme how the to run this and how to configure this? |
Adds RUNBOOK.md at the repo root covering the end-to-end operator flow: prerequisites (v5.4.0-pre.4 tari_suite, minotari-cli @ 52a7287a, PP submodule rev f0572c9, --recurse-submodules), one-time setup (build, gen-seed times three, print-address times three, view-key/spend-key extraction, environment file), full harness.toml key reference with defaults and override guidance, funding flow (11k tXTM per wallet, verify via console_wallet get-balance), run invocation, output schema pointer, and a troubleshooting section that mirrors the wallet-pain findings from analysis/PR_BODY_v2.md §4 as operator-facing gotchas. The troubleshooting section also points at three runtime bugs the post-review fixes address: console_wallet wait_ready gating on has_done_initial_validation, Mode 2 wait_for_balance_positive after scan subprocess exits, and the PR daemon --database-path sqlite3 file fix. Each maps the upstream-source cause to the symptom an operator would see and the current-code resolution. The runbook is a working doc for canonical-baseline operators (@SWvheerden during review, anyone reproducing the bench on their own hardware). No source code changes. Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
Added — RUNBOOK.md at ed706ec. Re: end-to-end validation against Esmeralda — blocked on chain availability this week. rpc.esmeralda.tari.com has been returning 503 intermittently. Local node sync from seednodes.json peers caps at height ~294k with a Nov-2025 tip timestamp; v5.4.0-pre.4 rejects the next block with |
|
with the view key setup, you can do this automatically? |
|
There is also no terminal feedback of it running? |
|
@SWvheerden both fair, will address.
For Esmeralda end-to-end validation, the public RPC has been 503 intermittently this week and local sync from seednodes.json peers caps at height ~294k with a Nov-2025 tip timestamp; v5.4.0-pre.4 rejects the next block with |
|
More feedback will def help, it fails here: |
|
@SWvheerden on the pre-flight failure, that's a. Is the OLD seed funded? b. What does the wallet itself report when run standalone against the same seed? c. RPC / peer state on your end? Happy to loosen the gate (e.g. accept Online alone after a shorter timeout, then fall through to a balance read that returns whatever the wallet currently sees) if the per-seed cold-scan time on a healthy network is genuinely above 30 min. The hard gate was added to fix enforce_funding falsely reporting 0 against an in-flight scan; a softer variant that warns instead of bailing is reasonable. |
Adds `derive_view_spend_keypair(mnemonic)` to `src/seed/mod.rs`, mirroring the existing `derive_address` derivation path: parse the mnemonic into a CipherSeed, wrap as a `SeedWordsWallet`, extract `(get_view_key, get_public_spend_key)`, and hex-encode both. Returns the `(view_private_key_hex, spend_public_key_hex)` pair the Mode 3 PR daemon needs for `minotari import-view-key` and PP's `ACCOUNTS__BENCH__*` env matrix. Wires the derive into `wallet_lifecycle::pp_lifecycle::resolve_account_keys` (was `read_account_env`), which now follows a clear resolution order: 1. Both env vars set → operator-injected override (lets operators inject keys derived from a wallet that differs from HARNESS_SEED_PP). 2. Neither set → auto-derive from HARNESS_SEED_PP. 3. Exactly one set → bail with a clear error (half an override is almost always a typo). `PpLifecycle::new` and both `PaymentProcessor` constructors call the resolver; the inline env reads are gone. Drops operator pre-flight config from 6 env vars to 4. The TARI_BENCH_VIEW_KEY and TARI_BENCH_SPEND_KEY env-var names stay defined in `Mode3Account` as operator-facing overrides; they no longer have to be set for a normal canonical-baseline run. 4 unit tests cover the derive function (hex shape, determinism, distinct-seeds, invalid-mnemonic). All 260 tests pass; sentinel test `builds_correct_argv_for_single_recipient` byte-identical. Notes: - Hex encoding uses `RistrettoSecretKey::reveal()` (returns a `RevealedSecretKey` whose Display is the hex form) and the `LowerHex` impl on `CompressedKey<RistrettoPublicKey>`. The `Hex` trait route via `tari_utilities` does not type-check here because our direct `tari_utilities = 0.8` and the transitive `tari_crypto = 0.23` → `tari_utilities = 0.10` resolve to two different ByteArray traits; the Display formatters bypass the mismatch and produce the same 64-char lowercase output. - The PR-daemon view-key wallet and PP's signer wallet both derive from the same seed, so unifying them under HARNESS_SEED_PP is semantically a no-op for the operator: the bench already requires the seed to be set; the env override path stays available for cross-wallet setups. Resolves @SWvheerden's 2026-06-19 review feedback on PR tari-project#6 (Work Item A from the operator's runbook). Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
A canonical Esmeralda baseline takes 3-5 hours; until now the harness ran silent until completion. Operators couldn't tell whether the run was making progress or stuck. Adds three terminal-feedback surfaces, all println!-based so they reach stdout regardless of RUST_LOG. Pre-flight (`guards::enforce_funding`): [HH:MM:SS] preflight old=NuT new=NuT pp=NuT PASS Scenario boundaries (run loop in main.rs): [HH:MM:SS] mode=<mode> scenario=<id> start [HH:MM:SS] mode=<mode> scenario=<id> done tx_count=N elapsed=N.Ns status=<ok|skipped|err> End-of-run summary table (`print_summary_table` after the per-mode loop, before `result_profile::write`): === run summary === mode\scenario B0 S0 S1 ... old_wallet skipped ok (1) ok (512) ... new_wallet ok (0) ok (1) ok (512) ... payment_processor skipped ok (1) skipped ... Two small helpers added in `main.rs`: - `mode_name(role)` — operator-facing label - `count_txs(outcome)` — best-effort per-scenario tx count derived from each `ScenarioOutcome` variant (S5's `success_count`; S4 reconstructs from `(n_concurrent * success_rate)`; B0/S2/S3/S6/S7 are scan-only and return 0). Not a load-bearing value — the canonical numbers live in the result-profile JSON. The lines use `chrono::Local` for the timestamp so they match the operator's wall clock. The summary table is a fixed-width text table that survives terminal narrowing; rendering is intentionally simple (no Unicode box-drawing, no colour) so it pastes cleanly into a PR comment. No source files outside `src/main.rs` and `src/guards.rs` are touched. All 260 tests pass; sentinel test `builds_correct_argv_for_single_recipient` byte-identical. Resolves @SWvheerden's 2026-06-19 review feedback on PR tari-project#6 (Work Item B from the operator's runbook). Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
…nbook Updates RUNBOOK §2.5 (Mode 3 view+spend extraction), §2.7 (env file), §3.5 (harness.toml accounts table), and adds §5.4 (terminal feedback) to match the two work items @SWvheerden asked for at 2026-06-19: §2.5 / §3.5: the TARI_BENCH_VIEW_KEY and TARI_BENCH_SPEND_KEY env vars are now optional. The harness auto-derives the pair from HARNESS_SEED_PP via wallet_lifecycle::pp_lifecycle::resolve_account_keys. The env-var path stays as an explicit operator override for the rare case of needing to scan a wallet that differs from HARNESS_SEED_PP. Half-overrides bail at startup. §2.7: the env file's TARI_BENCH_VIEW_KEY / TARI_BENCH_SPEND_KEY lines are commented out by default and labelled as optional overrides. §5.4 (new): documents the per-scenario stdout progress lines + final summary table the harness now prints regardless of RUST_LOG. Includes the example output an operator should expect to see, including the preflight line, per-scenario start/done lines, and the summary table. Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
7abf9fa restored two em-dashes (one in §2.5 paragraph prose, one in the §2.7 env-file comment). The runbook's standing convention is no em-dashes; replacing with a colon and a parenthetical clause matches the rest of the document. Signed-off-by: Dennis Vorobyov <roadhero@gmail.com>
|
Both items landed:
260/260 tests passing, sentinel byte-identical. Branch at 1a4f71e. Pre-flight diagnostic asks from the prior comment still open whenever you next look at the 1800s timeout you hit. |
Fixes #1.
§1. TL;DR
Mode 3 is now a real 3-process orchestrator (base node → PR daemon at
127.0.0.1:9146→ PP daemon at127.0.0.1:9145) per @SWvheerden's 2026-05-29 architectural clarification ("payment processor uses the new wallet, but its not the new wallet"). The previous shim that reused Mode 2's offline-sign pipeline is gone. All four open review threads from 5/29 are resolved (threads 4.1/4.2/4.3 tactical plus Mode 3 architectural). 253/253 tests passing,cargo fmt/clippy -D warnings/release build all clean.Workspace summary: 24 commits since the 5/29 status comment, breaking down as 2 Phase 4/5 tactical fixes, 16 Mode 3 architectural rework plus tests, and 6 post-review fixes addressing every BLOCKER/CONCERN from swe-review. Every commit is author-correct (Dennis Vorobyov roadhero@gmail.com) and DCO
Signed-off-byclean. The bounty-critical sentinel testbuilds_correct_argv_for_single_recipient(Mode 2 argv shape lock) is byte-identical across the entire 24-commit range. The canonical Esmeralda baseline is pending wallet funding; the funding address is listed in §5.§2. Changes since 5/29 status comment
§2.1. Tactical fixes (threads 4.1 / 4.2 / 4.3)
Thread 4.2 (
731dd7brefactor(modes): use MicroMinotari::from_str instead of custom decimal parser). Per @SWvheerden's review comment "microtari from string should do this" → "it should be on the type and from string". Deleted the bespoke decimal-shift currency parser and replaced it with a singleMicroMinotari::from_strcall. UpstreamMicroMinotari::FromStralready handles bothDisplayshapes ("100 µT"and"10.000001 T") round-trip, so no harness-side reimplementation is needed. Net-76 / +29lines.Threads 4.1 + 4.3 (
c368003feat(wallet_db): read UTXO counts from wallet sqlite3 instead of stderr). Per @SWvheerden's "I think bar some PR to minotari to add this feature, you have to dig into the database" and "I think this needs to be updated to search the wallet db". Theevent_count=Nstderr scrape is gone. Newsrc/wallet_db.rsmodule reads UTXO counts directly from the wallet's sqlite3 DB:count_outputs→SELECT COUNT(*) FROM outputs WHERE deleted_at IS NULL AND is_burn = 0count_spendable_utxos→ same predicate plusAND status = 'UNSPENT'Predicates mirror
minotari-cli@52a7287a/minotari/src/db/outputs.rs:616'sget_output_totals_for_account. Modes 2/3 injectArc<dyn WalletDb>; production wiresLiveWalletDb(opens read-only viaSQLITE_OPEN_READ_ONLY | SQLITE_OPEN_NO_MUTEX, which is WAL-safe alongside the wallet's writer), tests injectFakeWalletDbwith canned counts.scan_from_birthdaynow populatesoutputs_found/utxo_countfrom the DB afterminotari Scanexits. The previouslast_scancache field is gone.9 unit tests against a tempfile schema fixture (empty / filtered / soft-deleted / burn / missing-DB / corrupted-DB) plus 2 DB-injection tests per mode (canned-count read plus DB-error propagation with mode context).
§2.2. Mode 3 architectural rework (22 commits)
The previous Mode 3 was a shim that reused Mode 2's
minotariCLI pipeline. Per @SWvheerden's "payment processor uses the new wallet, but its not the new wallet" (issue #1, 2026-05-29), that was wrong.minotari_payment_processoris a separate application that runs in parallel to aminotariwallet daemon, drives the wallet over HTTP, and shells out tominotari_console_walletfor signing. Mode 3 has been rewritten end-to-end to match that architecture.Headline points:
minotari daemonon:9146with a view-key wallet →minotari_payment_processoron:9145(same view+spend keypair on the signer side).vendor/minotari_payment_processor@ commitf0572c9(commit8edc23b). Migration SQL and OpenAPI shapes are pinned to this rev at build time.reqwest-backed HTTP client for the 4 PP REST endpoints (commitc0929d6). No code-gen step; matches the existing harness'sBroadcastertyped-surface pattern.8693676):rusqlite(already a harness dep, no new sqlx-cli dependency) plusinclude_str!against the submodule's 6.sqlfiles. Build-time pinning: if the submodule rev drifts,cargo buildfails fast.PrLifecycle(commit4ef5b44) andPpLifecycle(commitdaeb994): child-process management with SIGTERM, 5s grace, then SIGKILL teardown;Dropguards viatokio::process::Command::kill_on_drop(true).1f0ff29).send_single/send_batch_one_to_manyPOST to PP; B0/S2/S3/S6/S7 returnUnsupportedOperation(the runner maps toCellResult::NotRunper existing Mode 1 S5-batch-arm precedent); S0/S1/S4/S5 produce real measurements where the v4/v5 upstream constraint allows (§4 finding 1)."default"to matchminotari-cli@52a7287a/minotari/src/utils/init_wallet.rs:121hardcode (friendly_name.unwrap_or("default")). See §4 wallet-pain findings.wallet-decoderpath. Both daemons (PR plus PP signer'sconsole_wallet) consume the same pair.4c23e35through3207dfd) address every BLOCKER and CONCERN from swe-review:4c23e35rebuilds PR daemon argv against the realminotariCLI (resolves B2 and N2)b086846makesget_balanceandget_utxo_countreturnOk(0)so S0 runs its real work instead of being silently skipped (resolves B1)7976adeper-methodSkippedreasons (resolves C5)954bf8efunding pre-flight queries the PR daemon HTTP API for Mode 3 (resolves C4)e279cf6deletes the unusedsub_segments_msfield (resolves C3)3207dfdcorrects a stale doc comment onBulkPaymentRequest.account_name§3. Mode 3 architecture
§3.1. Process topology
Spawn order: base node (existing) → PR daemon (
PrLifecycle::spawnplus readiness probe) → apply migrations to freshpayments.db→ PP daemon (PpLifecycle::spawnplus readiness probe). Seesrc/modes/payment_processor.rs::start_external_servicesfor the canonical sequence.§3.2. PR daemon configuration
The PR daemon is bootstrapped via a one-shot
import-view-keyinvocation followed by the long-runningdaemonsubcommand. The argv shape is rebuilt against the realminotari-cli@52a7287a/minotari/src/cli.rs(per commit4c23e35):Two non-obvious shape rules to note:
--networkis a top-levelCliflag (before the subcommand), not a subcommand-scoped flag. The earlier draft put it on the subcommand and clap-parse-failed.import-view-keynordaemonaccepts--account-name. The imported wallet is hardcoded to account name"default"perinit_wallet.rs:121. See §4 wallet-pain finding 2.Canonical source:
src/wallet_lifecycle/pr_lifecycle.rs:144-156(daemon_argv) and:158-...(spawn).§3.3. PP child-process env
The harness emits the following env set on the
tokio::process::Commandfor PP (canonical source:src/wallet_lifecycle/pp_lifecycle.rs, env-name table verified againstvendor/minotari_payment_processor/minotari_payment_processor/src/config.rs:46-101):DATABASE_URL=sqlite://<mode3_dir>/payments.db: relativesqlite://form. Per 0xPepeSilvia's §2 gotcha, absolutefile://breaks sqlx compile-time checks.PAYMENT_RECEIVER=http://127.0.0.1:9146: points at the PR daemon.BASE_NODE=<Config::base_node_url>LISTEN_IP=127.0.0.1,LISTEN_PORT=9145: pinned to loopback for bench safety.TARI_NETWORK=esmeralda: allowlisted bycrate::guards::enforce_esmeralda.CONSOLE_WALLET_PATH=<minotari_console_wallet absolute path>CONSOLE_WALLET_BASE_PATH=<mode3_dir>/console_wallet/: dedicated subdir under the Mode 3 tempdir; sqlite-lock-safe by construction (separate from Mode 1's and Mode 2's data dirs).CONSOLE_WALLET_PASSWORD=harness_pp_password(fixture).ACCOUNTS__BENCH__NAME=default: operator-facing config KEY isBENCH(matches the §12 spec convention); VALUE isdefaultbecause that's the wallet nameinit_wallet.rs:121actually creates. PP's HTTP calls to the PR daemon hit/accounts/default/...and would 404 otherwise.ACCOUNTS__BENCH__VIEW_KEY=<hex>: env-indirected. The TOML stores the env-var name (defaultTARI_BENCH_VIEW_KEY); the hex itself lives only in env at run-time. Same pattern asseeds.old/new/payment_processor.ACCOUNTS__BENCH__PUBLIC_SPEND_KEY=<hex>: same indirection.REVEAL_PII=true: bench logs comparable per Brief §3.CWD is set to
<mode3_dir>so PP's hardcodedlogs/audit.log(10MB × 5 rolling files) lands inside the harness tempdir tree and gets cleaned onDrop. stdout/stderr are piped to<mode3_dir>/logs/mode3-<run-id>.log(one file per run).§3.4. Worker sleep overrides
BATCH_CREATOR_SLEEP_SECSUNSIGNED_TX_CREATOR_SLEEP_SECSTRANSACTION_SIGNER_SLEEP_SECSBROADCASTER_SLEEP_SECSCONFIRMATION_CHECKER_SLEEP_SECSAt PP defaults, end-to-end payment latency is ≥100s per payment. The 1s/5s overrides bring the bench numbers into a sane range. Each value is configurable per-instance via
[mode_3.worker_sleep_overrides]inharness.toml. PP defaults verified atvendor/minotari_payment_processor/minotari_payment_processor/src/workers/batch_creator.rs:12(600),unsigned_tx_creator.rs:34(15),transaction_signer.rs:18(10),broadcaster.rs:15(15),confirmation_checker.rs:18(60).§3.5. Migration handling
vendor/minotari_payment_processor/migrations/*.sql. The 6 files apply in lexical order:20251017081013_init.sql20251201115213_add_intermediate_context.sql20251209145502_add_payref_to_payments.sql20260102132241_add_index_to_payref.sql20260123142522_add_events_table.sql20260126120000_add_block_headers_table.sqlinclude_str!-embedded into the harness binary at build time. Pinning is structural: bumping the submodule re-reads the new files at the nextcargo build. No runtime filesystem scan; no operator-supplied migration path.rusqliteatPpLifecycle::spawn:rm -rf <mode3_dir>→mkdir -p→ openConnection→PRAGMA foreign_keys = ON→execute_batcheach SQL string in order → assert post-migration tables exist (payments,payment_batches,events,block_headers).src/pp_migrations.rs. 3 unit tests verify againsttempfile::TempDir:apply_migrations_creates_payments_table,apply_migrations_creates_all_required_tables,apply_migrations_is_idempotent_when_re_run.§3.6. HTTP client surface (
src/pp_http_client.rs)reqwest::ClientperPpHttpClientfor connection-pool reuse, mirroring the existingBroadcasterpattern (src/broadcast/mod.rs). Cloned viaArc<PpHttpClient>for S4 fan-out.submit_batch(account_name, items: Vec<BulkPaymentItem>) -> BulkPaymentResponse: POST/v1/payment-batches. Idempotent via(client_id, account_name). Hard cap of 100 items per batch (upstreamMAX_BATCH_SIZE); the harness errors loudly above this.poll_payment(payment_id) -> PaymentResponse: GET/v1/payments/{id}.stream_events(filters) -> EventListResponse: GET/v1/events. Poll-based (no SSE upstream).health_version() -> ServiceVersion: GET/health/version. Used as the readiness probe (200ms backoff, 30s timeout; ~10× headroom against PepeSilvia's observed 2-3s PP boot).Broadcaster/WalletDb).PaymentStatusenum mirrors upstreamvendor/minotari_payment_processor/minotari_payment_processor/src/db/payment.rs:14-22verbatim:#[serde(rename_all = "SCREAMING_SNAKE_CASE")]with 5 variants (Received,Batched,Confirmed,Failed,Cancelled).client_id; retries would skew throughput measurement. Transport errors return as-is to the caller, which records them as scenario failures per the "no harness retries" AC.§4. Wallet pain findings
This section is the maintainer-facing artifact for what the operator learned about the upstream wallet stack while building Mode 3 against it. Per @SWvheerden's review framing ("I think bar some PR to minotari to add this feature, you have to dig into the database" and "it should be on the type and from string", both PR #6 inline comments from 2026-05-29), both directions belong here as first-class findings: "upstream lacks X, harness has to compensate" and "upstream already has X, harness should use it".
1. v4/v5 signer toolchain mismatch (Mode 3 PR daemon → PP signer worker).
minotari create-unsigned-transaction(the PR daemon side) emits unsigned-tx JSON withversion: 4.0.0.minotari_console_wallet sign-one-sided-transaction(the PP signer worker) rejects it expectingversion: 5.0.0. PP's signer worker retries the failing batch everyTRANSACTION_SIGNER_SLEEP_SECSseconds with no max-attempts cap. Result: payments stall atSigningInProgressindefinitely. Evidence:vendor/minotari_payment_processor/minotari_payment_processor/src/workers/transaction_signer.rs:235-289(shells out to console_wallet); empirically reproduced by 0xPepeSilvia'stari-wallet-benchmarksfork (theirwallet_pain_findings.mdFinding #1). Impact on canonical baseline: Mode 3 S5/S0 batches will reachAwaitingSignature/SigningInProgressand stop. Ingest throughput (POST /v1/payment-batches) is measurable; end-to-end settled is not. This is an upstream toolchain bug, not a harness defect.2. No
--account-nameoverride onimport-view-keyordaemon.minotari-cli@52a7287a/minotari/src/cli.rs:273definesImportViewKeywithSecurityArgsplusDatabaseArgsflatten plusview_private_keyplusspend_public_keyplusbirthday, with noAccountArgsflatten. SimilarlyCommands::Daemonatcli.rs:222flattensSecurityArgsplusNodeArgsplusDatabaseArgsonly, with noAccountArgs. The created/served wallet's account name is hardcoded to"default"perminotari/src/utils/init_wallet.rs:121(friendly_name.unwrap_or("default")). Impact: PP'sACCOUNTS__BENCH__NAMEenv VALUE must be"default"(not the operator-natural"bench") or PP's HTTP calls to the PR daemon hit 404. The harness now flips this VALUE (KEY staysBENCH). Resolved in commit4c23e35.3. Secrets passed via argv (visible in
ps -ef). Bothimport-view-keyanddaemonaccept--passwordas a CLI flag.import-view-keyalso takes--view-private-keyand--spend-public-keyas CLI args. On a multi-user host these are visible viaps -ef,/proc/<pid>/cmdline, and process accounting. The view-key alone reveals every incoming amount/address for the account; the password unlocks the wallet DB. The harness's existing pattern (SeedHandle::wallet_passwordreads from env at use site) is the established convention. Upstream CLI doesn't appear to accept env-var fallbacks for these flags. I'll confirm this during the canonical-baseline run. Impact: documented limitation; defense-in-depth follow-up is to either probe upstream for env fallbacks or wrap argv in a privacy shield.4. PP audit log relative path.
vendor/minotari_payment_processor/...writeslogs/audit.log(rolling, 10MB × 5 files) relative to its CWD. Hardcoded, with no env override. The harness sets PP's CWD to its per-modeHarnessDataDirso the logs land inside the harness tempdir and get cleaned onDrop. Worth surfacing for upstream if multi-instance PP deployments matter.5.
MicroMinotari::FromStrround-trips bothDisplayshapes cleanly (positive finding). Per @SWvheerden's review comment "it should be on the type and from string",tari_transaction_components::tari_amount::MicroMinotariimplementsFromStrthat handles bothDisplayoutputs round-trip:"100 µT"and"10.000001 T". The previous harness implementation had a 60-line custom decimal-shift parser for theTform. Replaced with a singleMicroMinotari::from_str().map(|m| m.as_u64())call in commit731dd7b. Wallet learning: the upstream type is the right abstraction; don't reimplement.6. Wallet DB canonical UTXO-count predicate. Per @SWvheerden's review comment "I think bar some PR to minotari to add this feature, you have to dig into the database", the wallet's sqlite3
outputstable is queried withWHERE status = 'UNSPENT' AND deleted_at IS NULL AND is_burn = 0for the canonical UTXO count (matchesminotari-cli@52a7287a/minotari/src/db/outputs.rs:616'sget_output_totals_for_account). The harness'swallet_dbmodule mirrors this exactly (commitc368003). Wallet learning: the previous stderrevent_count=Nscrape was rejected by review for good reason. The DB is the authoritative source.§5. Known limitations
Canonical Esmeralda baseline for Mode 3 pending wallet funding. Funding address (PR daemon side, single watched account, view+spend keypair derived from the operator's testnet seed):
Once funded, S0/S4/S5 run against testnet and
baseline_profile.jsonlands as a follow-up commit on this branch. Mode 3 will reachAwaitingSignatureand stop per §4 finding 1; the result profile records this as the bench's known upstream finding (see §6).Modes 1 and 2 baseline is independently producible right now. Their existing seeds (addresses in the original PR body) are funded-ready and don't depend on the Mode 3 PR daemon. If you'd prefer a partial baseline ahead of Mode 3 funding, I can run those two arms first and attach the profile, then ship the Mode 3 cells once funded.
enforce_fundingwarn-and-skip for Mode 3 PR daemon. Pre-flight runs at startup, before the per-mode lifecycle spawn. If the PR daemon'sGET /accounts/default/balanceendpoint isn't reachable (connection refused), the harness logs a warn explaining the cause and skips the Mode 3 arm of the funding check rather than bailing the whole run. Trades pre-flight strictness for ergonomics. Operators who want strict Mode 3 pre-flight coverage can pre-warm the PR daemon out-of-band before invoking the harness. Implementation:src/guards.rsplussrc/wallet_lifecycle/pr_balance_query.rs(commit954bf8e).PR seed strict-bail unwired (spec §16.4 row 4). The pre-implementation spec called for a strict bail when the PR daemon's account balance is zero. The HTTP-shaped pre-flight is wired (
src/wallet_lifecycle/pr_balance_query.rs), but the strict-bail policy is unwired; currently the warn-and-skip above is the only outcome. Deferred to canonical-baseline review per spec §16.4.S4 dispatcher terminal-state poll.
PpDispatcherdoesn't append tosubmitted_payment_ids; the shutdown poll loop sees only sequentialsend_*ids, not S4 fan-out submissions. Spec §11 explicitly marks S4 as throughput-only ("S4 is a throughput measurement, terminal-state tracking is best-effort there"). Documented here so the maintainer can request a tightening if needed.C6 redaction defense-in-depth gap. The harness's R3/R4 redaction regex (
src/seed/redact.rs) requires the literal substringview-key:(orview_key,view key) before the 64-char hex. PP's piped stdout/stderr log goes through this redaction; if PP debug-prints a raw 64-char hex without the label, the redaction passes it through. Defense-in-depth follow-up: whenConfig::mode_3isSome, add the runtime hex values themselves as substring rules.harness.toml.exampleplacement. Currently at repo root for first-PR visibility. Maintainer-preferred location (e.g.examples/ordocs/) is unconfirmed; happy to move during review.§6. Open question for @SWvheerden
If the v4/v5 mismatch (§4 finding 1) terminates all Mode 3 batches at
AwaitingSignatureduring the canonical baseline, the harness will emit realfailed-statusTxRecords with the actual error string. The "Harness does not hide wallet pain" AC suggests this IS the intended outcome. Please confirm before the canonical run, or specify the preferred reporting shape for upstream-stalled cells.§7. CI state
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo nextest runcargo build --releasebuilds_correct_argv_for_single_recipientgit show 3207dfd:src/modes/minotari_subprocess.rs↔git show origin/main:src/modes/minotari_subprocess.rson the test fn)git rebase --exec 'cargo check'over the 24 commits§8. Commit map
24 commits since the 5/29 status comment, grouped into three phases.
Phase 4/5 tactical (per-commit)
731dd7brefactor(modes): use MicroMinotari::from_str instead of custom decimal parser(thread 4.2)c368003feat(wallet_db): read UTXO counts from wallet sqlite3 instead of stderr(threads 4.1 + 4.3)Mode 3 rework (per-commit)
8edc23bchore(vendor): add minotari_payment_processor submodule(pinned @f0572c9)ae7a59afeat(config): add Mode3Config + WorkerSleepOverridesc0929d6feat(pp_http_client): hand-rolled reqwest client for PP REST API8693676feat(pp_migrations): apply vendored PP sqlite migrations inline4ef5b44feat(wallet_lifecycle): PrLifecycle for Mode 3 PR daemondaeb994feat(wallet_lifecycle): PpLifecycle for Mode 3 payment processorab48f9afeat(modes): extend TxRecord with optional sub_segments_ms(later removed ine279cf6post-review as unused)1f0ff29feat(modes): rewrite Mode 3 as 3-process HTTP orchestrator2d4c03ffeat(config): startup validation for Mode 3 + harness.toml example3bcb9cdtest(pp_migrations): unit tests for apply_migrations0a98e8atest(pp_http_client): wiremock coverage for the 4 PP REST endpoints865e324test(pp_lifecycle): fake-binary spawn/teardown/drop coveragea3bb8d8test(pr_lifecycle): argv shape + fake-binary spawn/teardown/drop coverageb9a690etest(modes/payment_processor): Mode 3 trait + wiremock + dispatcher coverageae832d3test(integration): Mode 3 skip-scan + PpLifecycle-with-fake-binarycd85b20style: apply rustfmt to swe-test additionsPost-review fixes (per-commit)
4c23e35fix(pr_lifecycle): rebuild daemon argv against real minotari CLI + unify account name(resolves swe-review B2 + N2)b086846fix(payment_processor): get_balance / get_utxo_count return Ok(0) instead of UnsupportedOperation(resolves B1; S0 was being silently skipped before its real work)7976adefix(payment_processor): per-method Skipped reasons(resolves C5)954bf8efix(guards): funding pre-flight queries PR daemon for Mode 3(resolves C4)e279cf6refactor(tx_record): delete unused sub_segments_ms field(resolves C3; the field added inab48f9aended up unused once events were threaded differently)3207dfddocs(pp_http_client): correct stale account_name commentNote: 6 pre-5/29 commits (
fe9b39cthrough3241d26) are also in this PR from earlier work. They are stable and unchanged since the 5/29 status comment.§9. Don't-touch tests
src/modes/minotari_subprocess.rs::tests::builds_correct_argv_for_single_recipient(sentinel): verified byte-identical across the 24 commits since 5/29. This test is the AC-pinned argv shape and locks the public contract onminotari_subprocess; touching it would break the bounty's "argv shape is stable" invariant. Confirmed viagit show 3207dfd:src/modes/minotari_subprocess.rsvsgit show origin/main:src/modes/minotari_subprocess.rson the test fn body.Relates-to: #1