Skip to content

feat(us1): phase 3 — first launch & identity setup - #52

Merged
grunch merged 2 commits into
mainfrom
004-mostro-p2p-client
Mar 29, 2026
Merged

feat(us1): phase 3 — first launch & identity setup#52
grunch merged 2 commits into
mainfrom
004-mostro-p2p-client

Conversation

@grunch

@grunch grunch commented Mar 29, 2026

Copy link
Copy Markdown
Member

Rust crypto layer:

  • keys.rs: BIP-39 mnemonic generation + BIP-32 derivation at m/44'/1237'/38383'/0/N for identity and trade keys
  • ecdh.rs: NIP-44 v2 encrypt/decrypt + k256 diffie_hellman for raw shared-secret derivation (file-attachment encryption)
  • nym.rs: deterministic pseudonym from SHA-256(pubkey) — adjective-noun + icon index 0–36 + hue 0–359

Identity API (rust/src/api/identity.rs):

  • create_identity, load_identity_from_mnemonic, import_from_mnemonic, import_from_nsec, get/delete identity, derive/get trade key, get_nym_identity, export_encrypted_backup
  • In-memory OnceLock<RwLock<Option>>; mnemonic never persisted by Rust

Flutter US1:

  • WalkthroughScreen: 6 slides via introduction_screen, custom dots (pill 16×8 active, circle 8×8 inactive), Done/Skip navigation
  • HighlightConfig: per-slide regex patterns for green semibold terms in EN/ES/IT/FR/DE
  • firstRunProvider + backupReminderProvider via shared_preferences
  • GoRouter redirect: firstRunComplete=false → /walkthrough; routerContainer wired in MostroApp for context-free provider reads

Summary by CodeRabbit

  • New Features

    • Interactive first-time onboarding with walkthrough and backup reminder
    • First-run state handling and persistent first-run completion
    • Identity creation, import/export (encrypted backup), and trade-key management
    • Encrypted peer-to-peer messaging support
    • Deterministic user pseudonym (nym) generation
  • Bug Fixes

    • Fixed timestamp extraction in order event processing
  • Chores

    • Updated cryptographic dependencies
    • Marked Phase 3 tasks as completed

Rust crypto layer:
- keys.rs: BIP-39 mnemonic generation + BIP-32 derivation at
  m/44'/1237'/38383'/0/N for identity and trade keys
- ecdh.rs: NIP-44 v2 encrypt/decrypt + k256 diffie_hellman for
  raw shared-secret derivation (file-attachment encryption)
- nym.rs: deterministic pseudonym from SHA-256(pubkey) —
  adjective-noun + icon index 0–36 + hue 0–359

Identity API (rust/src/api/identity.rs):
- create_identity, load_identity_from_mnemonic, import_from_mnemonic,
  import_from_nsec, get/delete identity, derive/get trade key,
  get_nym_identity, export_encrypted_backup
- In-memory OnceLock<RwLock<Option<IdentityState>>>; mnemonic never
  persisted by Rust

Flutter US1:
- WalkthroughScreen: 6 slides via introduction_screen, custom dots
  (pill 16×8 active, circle 8×8 inactive), Done/Skip navigation
- HighlightConfig: per-slide regex patterns for green semibold terms
  in EN/ES/IT/FR/DE
- firstRunProvider + backupReminderProvider via shared_preferences
- GoRouter redirect: firstRunComplete=false → /walkthrough;
  routerContainer wired in MostroApp for context-free provider reads
@grunch

grunch commented Mar 29, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 29, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Mar 29, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 688b261a-24a1-450a-84b6-848138c16029

📥 Commits

Reviewing files that changed from the base of the PR and between c54d8df and 0c26ec6.

⛔ Files ignored due to path filters (1)
  • rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • lib/features/walkthrough/providers/first_run_provider.dart
  • lib/features/walkthrough/screens/walkthrough_screen.dart
  • rust/Cargo.toml
  • rust/src/api/identity.rs
  • rust/src/crypto/ecdh.rs
  • rust/src/crypto/nym.rs

Walkthrough

Sets up router redirect to read Riverpod first-run state via a global routerContainer, adds a 6-slide Walkthrough UI with highlighted terms, and implements Rust identity, key derivation, ECDH, encrypted backups, and deterministic nym generation.

Changes

Cohort / File(s) Summary
App Routing
lib/core/app.dart, lib/core/app_routes.dart
Assigns global ProviderContainer? routerContainer in app build and adds router redirect that reads firstRunProvider to force walkthrough when firstRunComplete is false.
Walkthrough UI & State
lib/features/walkthrough/screens/walkthrough_screen.dart, lib/features/walkthrough/utils/highlight_config.dart, lib/features/walkthrough/providers/first_run_provider.dart
Adds WalkthroughScreen (6 slides), HighlightConfig for per-slide regex highlighting, firstRunProvider (AsyncValue) backed by SharedPreferences, and backupReminderProvider.
Rust Crypto: keys & ECDH
rust/src/crypto/keys.rs, rust/src/crypto/ecdh.rs, rust/src/crypto/mod.rs
Adds BIP-39/BIP-32 mnemonic handling, identity/trade key derivation, ECDH-based shared-key derivation and NIP-44 V2 encrypt/decrypt utilities, and exposes crypto submodules.
Rust Nym Identity
rust/src/crypto/nym.rs
Adds deterministic nym identity derivation from a Nostr public key (pseudonym, icon_index, color_hue) with unit tests.
Rust Identity API
rust/src/api/identity.rs, rust/src/api/mod.rs, rust/Cargo.toml
New in-memory identity API: create/load/import/delete identity, derive trade keys, export encrypted mnemonic backups (ChaCha20-Poly1305), expose api::identity, and add k256/rand deps.
Misc Rust & Docs
rust/src/nostr/order_events.rs, specs/004-mostro-p2p-client/tasks.md
Minor timestamp extraction change in order event parsing; mark Phase 3 tasks as completed.

Sequence Diagram

sequenceDiagram
    participant App as MostroApp
    participant Router as AppRouter
    participant Provider as firstRunProvider
    participant Persist as SharedPreferences
    participant Screen as WalkthroughScreen

    App->>Router: set global routerContainer via ProviderScope
    Router->>Provider: read firstRunProvider (async)
    Provider->>Persist: load `_kFirstRunComplete` (default false)
    Persist-->>Provider: return persisted value
    Provider-->>Router: AsyncValue<bool> (firstRunComplete)
    alt firstRunComplete == false
        Router->>Screen: redirect to WalkthroughScreen
        Screen->>Provider: markFirstRunComplete()
        Provider->>Persist: save firstRunComplete = true
        Provider-->>Screen: state updated
        Screen->>Router: navigate to AppRoute.home
    else
        Router->>App: proceed with requested route
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #51: Alters MostroApp.build and app_routes to wire a global Provider container for router redirects — directly related to routing-state integration here.
  • PR #17: Implements get_nym_identity/nym derivation logic that matches the deterministic nym feature added in rust/src/crypto/nym.rs.
  • PR #50: Adds/expands Rust crypto & API stubs and Flutter wiring that this PR fills out with real implementations (keys, ecdh, identity API).

Poem

🐰 I hopped through slides with highlighted cheer,

six tiny screens to make the path clear;
Rust keys hummed softly, a cryptic tune,
nym names blossomed under the moon,
backup tucked safe — welcome, friend, here!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(us1): phase 3 — first launch & identity setup' directly aligns with the PR's primary objectives: implementing first-run onboarding (WalkthroughScreen) and identity setup (crypto layer, identity API, Riverpod state management).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 004-mostro-p2p-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
rust/src/crypto/ecdh.rs (1)

88-97: Document the even-Y parity assumption.

The function always uses 0x02 prefix (even Y), which is correct per BIP-340/Nostr convention for x-only pubkeys. A brief comment would clarify this design decision for future maintainers.

📝 Add clarifying comment
 /// Convert a 32-byte x-only pubkey to a 33-byte compressed SEC1 point.
+/// Per BIP-340 and Nostr convention, x-only keys assume even Y parity.
 fn compress_from_xonly(xonly: &[u8]) -> Result<Vec<u8>> {
     if xonly.len() != 32 {
         return Err(anyhow!("expected 32-byte x-only pubkey, got {}", xonly.len()));
     }
     let mut out = Vec::with_capacity(33);
-    out.push(0x02); // assume even Y (matches Nostr convention)
+    out.push(0x02); // Even Y parity per BIP-340/Nostr x-only convention
     out.extend_from_slice(xonly);
     Ok(out)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/crypto/ecdh.rs` around lines 88 - 97, The compress_from_xonly
function currently always prepends 0x02 (even Y) when converting a 32-byte
x-only pubkey to a 33-byte compressed SEC1 point; add a brief clarifying comment
above/composed into compress_from_xonly explaining that this hardcoded 0x02
implements the BIP-340/Nostr convention of treating the x-only pubkey as having
an even Y coordinate, so maintainers know the parity assumption is intentional
and not a bug.
rust/src/crypto/nym.rs (1)

72-97: Consider adding a fixed test vector for regression testing.

The current tests use randomly generated keys, which validates properties but doesn't catch accidental changes to the derivation algorithm. A fixed pubkey with expected output would catch regressions.

🧪 Proposed test with fixed vector
+    #[test]
+    fn known_pubkey_produces_known_nym() {
+        // Fixed test vector to catch algorithm changes
+        let hex = "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2";
+        let nym = get_nym_identity(hex).unwrap();
+        // Update these expected values once after initial implementation
+        assert!(!nym.pseudonym.is_empty());
+        assert!(nym.icon_index <= 36);
+        assert!(nym.color_hue <= 359);
+        // Optionally pin exact values:
+        // assert_eq!(nym.pseudonym, "expected-nym");
+        // assert_eq!(nym.icon_index, expected_index);
+        // assert_eq!(nym.color_hue, expected_hue);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/crypto/nym.rs` around lines 72 - 97, Add a deterministic regression
test using a fixed public key hex and its expected NymIdentity to catch
algorithm regressions: create a new test (or extend existing
same_pubkey_same_identity) that calls get_nym_identity with a hard-coded public
key hex string and asserts equality against the known expected pseudonym,
icon_index, and color_hue values (referencing get_nym_identity,
same_pubkey_same_identity, icon_index_in_range, color_hue_in_range to locate
test area); if get_nym_identity returns a Result, unwrap or handle the error
consistently and lock the expected values into the test so any future change in
derivation fails the test.
lib/features/walkthrough/providers/first_run_provider.dart (1)

31-41: Consider refactoring to avoid passing WidgetRef into the StateNotifier.

Passing WidgetRef to markFirstRunComplete couples this notifier to widget-layer concerns. A cleaner pattern would be to inject backupReminderProvider.notifier via the provider's ref at construction time, or have the caller invoke both notifiers separately.

This works correctly as-is, but could be improved for testability and separation of concerns.

♻️ Suggested refactor to remove WidgetRef dependency
 final firstRunProvider =
     StateNotifierProvider<FirstRunNotifier, AsyncValue<bool>>(
-  (ref) => FirstRunNotifier(),
+  (ref) => FirstRunNotifier(ref),
 );

 class FirstRunNotifier extends StateNotifier<AsyncValue<bool>> {
-  FirstRunNotifier() : super(const AsyncValue.loading()) {
+  FirstRunNotifier(this._ref) : super(const AsyncValue.loading()) {
     _load();
   }

+  final Ref _ref;
+
   // ... _load() unchanged ...

   /// Mark the walkthrough as completed. Called by both "Done" and "Skip".
   ///
   /// Also activates the backup reminder (red dot on notification bell).
-  Future<void> markFirstRunComplete(WidgetRef ref) async {
+  Future<void> markFirstRunComplete() async {
     final prefs = await SharedPreferences.getInstance();
     await prefs.setBool(_kFirstRunComplete, true);
     state = const AsyncValue.data(true);

     // Activate persistent backup reminder notification.
-    ref.read(backupReminderProvider.notifier).showBackupReminder();
+    _ref.read(backupReminderProvider.notifier).showBackupReminder();
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/walkthrough/providers/first_run_provider.dart` around lines 31 -
41, The markFirstRunComplete method currently accepts a WidgetRef which couples
the StateNotifier to widget-layer APIs; refactor by removing the WidgetRef
parameter from markFirstRunComplete and instead inject the backup reminder
notifier at construction time (e.g., accept a BackupReminderNotifier or
AutoDisposeNotifierBase instance in the FirstRunProvider/StateNotifier
constructor obtained from the provider ref when creating the notifier), or
alternatively leave markFirstRunComplete to only update SharedPreferences and
state and require callers to separately call
backupReminderProvider.notifier.showBackupReminder(); update all call sites that
used markFirstRunComplete(ref) to call the new signature or to call the backup
reminder separately and ensure tests construct the notifier with a mock/injected
backup reminder instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rust/src/api/identity.rs`:
- Around line 183-197: The code in derive_trade_key currently increments
state.identity_info.trade_key_index before calling key_ops::derive_trade_key,
which can leave the stored index advanced if derivation fails; change the flow
in derive_trade_key to call key_ops::derive_trade_key(&state.mnemonic_words,
candidate_index) first (using a local candidate_index computed as
state.identity_info.trade_key_index + 1), only increment and persist
state.identity_info.trade_key_index after a successful derivation, and then
construct and return the TradeKeyInfo from the derived keys; reference
derive_trade_key, state.identity_info.trade_key_index,
key_ops::derive_trade_key, and TradeKeyInfo to locate the changes.
- Around line 251-261: The code uses a fixed all-zero nonce (variable nonce)
with ChaCha20Poly1305 which is unsafe; change the encryption to generate a
cryptographically random 12-byte nonce (e.g., via rand::rngs::OsRng) per
encryption, use that nonce with ChaCha20Poly1305::encrypt, and persist/serialize
the nonce along with the ciphertext (prepend or include in envelope) so decrypt
can use the same nonce; update dependencies to include rand and ensure the code
paths around key_bytes, cipher, nonce, and encrypt handle the nonce being random
and stored with the ciphertext.
- Around line 112-119: The IdentityInfo creation currently sets created_at to
unix_now() which overwrites the original Flutter-persisted creation time; update
the code in the function that constructs IdentityInfo (e.g.,
load_identity_from_mnemonic / the block that builds IdentityInfo using
public_key) to accept and use a passed-in created_at timestamp from Flutter
instead of always calling unix_now(), and only fall back to unix_now() when the
incoming created_at is absent or invalid so original creation time is preserved.

In `@rust/src/crypto/ecdh.rs`:
- Around line 15-39: The function derive_shared_key currently returns the NIP-04
style SHA256(ECDH_x) (via ecdh_sha256) while also deriving a NIP-44
ConversationKey and discarding it; fix by either (A) renaming the function and
its docs to reflect NIP-04 semantics (e.g., derive_nip04_shared_key or
derive_ecdh_sha256) and keep returning ecdh_sha256, or (B) actually return the
NIP-44 v2 conversation key bytes: use
nostr_sdk::nips::nip44::v2::ConversationKey::derive (as already called) and then
materialize its raw key bytes by using a stable approach available in this SDK
version (e.g., if raw extraction isn’t exposed, perform a deterministic
encrypt/decrypt round-trip or call any provided export method) and replace the
ecdh_sha256 return with those HKDF-derived bytes; update function name/docs
accordingly (refer to derive_shared_key, ConversationKey, and ecdh_sha256 to
locate edits).

---

Nitpick comments:
In `@lib/features/walkthrough/providers/first_run_provider.dart`:
- Around line 31-41: The markFirstRunComplete method currently accepts a
WidgetRef which couples the StateNotifier to widget-layer APIs; refactor by
removing the WidgetRef parameter from markFirstRunComplete and instead inject
the backup reminder notifier at construction time (e.g., accept a
BackupReminderNotifier or AutoDisposeNotifierBase instance in the
FirstRunProvider/StateNotifier constructor obtained from the provider ref when
creating the notifier), or alternatively leave markFirstRunComplete to only
update SharedPreferences and state and require callers to separately call
backupReminderProvider.notifier.showBackupReminder(); update all call sites that
used markFirstRunComplete(ref) to call the new signature or to call the backup
reminder separately and ensure tests construct the notifier with a mock/injected
backup reminder instance.

In `@rust/src/crypto/ecdh.rs`:
- Around line 88-97: The compress_from_xonly function currently always prepends
0x02 (even Y) when converting a 32-byte x-only pubkey to a 33-byte compressed
SEC1 point; add a brief clarifying comment above/composed into
compress_from_xonly explaining that this hardcoded 0x02 implements the
BIP-340/Nostr convention of treating the x-only pubkey as having an even Y
coordinate, so maintainers know the parity assumption is intentional and not a
bug.

In `@rust/src/crypto/nym.rs`:
- Around line 72-97: Add a deterministic regression test using a fixed public
key hex and its expected NymIdentity to catch algorithm regressions: create a
new test (or extend existing same_pubkey_same_identity) that calls
get_nym_identity with a hard-coded public key hex string and asserts equality
against the known expected pseudonym, icon_index, and color_hue values
(referencing get_nym_identity, same_pubkey_same_identity, icon_index_in_range,
color_hue_in_range to locate test area); if get_nym_identity returns a Result,
unwrap or handle the error consistently and lock the expected values into the
test so any future change in derivation fails the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 15a91bb9-20fe-4e9d-8597-514d00287990

📥 Commits

Reviewing files that changed from the base of the PR and between a9b8a99 and c54d8df.

⛔ Files ignored due to path filters (1)
  • rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • lib/core/app.dart
  • lib/core/app_routes.dart
  • lib/features/walkthrough/providers/first_run_provider.dart
  • lib/features/walkthrough/screens/walkthrough_screen.dart
  • lib/features/walkthrough/utils/highlight_config.dart
  • rust/Cargo.toml
  • rust/src/api/identity.rs
  • rust/src/api/mod.rs
  • rust/src/crypto/ecdh.rs
  • rust/src/crypto/keys.rs
  • rust/src/crypto/mod.rs
  • rust/src/crypto/nym.rs
  • rust/src/nostr/order_events.rs
  • specs/004-mostro-p2p-client/tasks.md

Comment thread rust/src/api/identity.rs Outdated
Comment thread rust/src/api/identity.rs
Comment thread rust/src/api/identity.rs
Comment thread rust/src/crypto/ecdh.rs Outdated
identity.rs:
- derive_trade_key: compute candidate_index before calling derivation;
  only persist incremented trade_key_index on success to avoid
  advancing the counter on a failed key derivation
- load_identity_from_mnemonic: accept created_at: Option<i64> so the
  original Flutter-persisted creation timestamp is preserved on reload;
  falls back to unix_now() when absent or zero
- export_encrypted_backup: replace fixed zero nonce with OsRng random
  12-byte nonce per call; prepend nonce to ciphertext before base64
  encoding (format: [12-byte nonce][ciphertext+tag]); add rand dep

ecdh.rs:
- rename derive_shared_key → derive_nip04_shared_key and rewrite docs
  to accurately reflect NIP-04 semantics (SHA-256 of ECDH x-coord);
  remove dead ConversationKey derivation that was silently discarded
- compress_from_xonly: expand comment to cite BIP-340/Nostr as source
  of the unconditional even-Y assumption

nym.rs:
- add known_pubkey_regression test with secp256k1 generator pubkey;
  asserts pseudonym="tall-crane", icon_index=35, color_hue=249 to
  catch any future algorithm drift

first_run_provider.dart:
- remove WidgetRef parameter from markFirstRunComplete to decouple
  StateNotifier from widget-layer APIs; call site activates
  backupReminderProvider separately
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant