Skip to content

fix: never offer the correct seed word twice when verifying - #62

Open
munzzyy wants to merge 1 commit into
cake-tech:mainfrom
munzzyy:fix/verify-seed-duplicate-word
Open

fix: never offer the correct seed word twice when verifying#62
munzzyy wants to merge 1 commit into
cake-tech:mainfrom
munzzyy:fix/verify-seed-duplicate-word

Conversation

@munzzyy

@munzzyy munzzyy commented Aug 3, 2026

Copy link
Copy Markdown

VerifySeedViewModel.randomWords builds the options for the pick-the-correct-word screen by taking 5 words off the top of a shuffled wordlist and then appending the answer. When the answer is already one of those 5 it ends up on the screen twice. The elms.toSet() line that was meant to catch that throws its result away, so it never removed anything.

The same closure shuffles wordList in place instead of a copy. info_page.dart passes Bip39.english straight in, and that is a static list, so every seed verification permanently reorders the wordlist for the rest of the session.

Repro

test/scratch_verify_seed_test.dart on current main:

import 'package:cupcake/utils/bip39.dart';
import 'package:cupcake/view_model/verify_seed_view_model.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('correct word is never offered twice', () {
    final vm = VerifySeedViewModel(
      seedWords: ['alpha'],
      wordList: ['alpha', 'bravo', 'charlie', 'delta', 'echo'],
    );
    final options = vm.randomWords;
    print('options   : $options');
    print('duplicate : ${options.length != options.toSet().length}');
  });

  test('Bip39.english keeps its order', () {
    final before = List<String>.of(Bip39.english);
    VerifySeedViewModel(seedWords: ['zoo'], wordList: Bip39.english).randomWords;
    print('first five: ${Bip39.english.take(5).toList()}');
    print('reordered : ${Bip39.english.join(' ') != before.join(' ')}');
  });
}

Before:

options   : [delta, alpha, charlie, echo, alpha, bravo]
duplicate : true
first five: [prize, thank, humor, boring, chef]
reordered : true

After:

options   : [delta, bravo, echo, alpha, charlie]
duplicate : false
first five: [abandon, ability, able, about, above]
reordered : false

The 5-word list in the first test is what makes the duplicate deterministic. In the app it needs the answer to land in the five decoys, which is rare but not rare enough to leave alone.

With Bip39.english the option count is unchanged. Three runs of VerifySeedViewModel(seedWords: ['abandon', 'zoo', 'ability'], wordList: Bip39.english) after the fix:

count=6 unique=6 answer=ability contains=true
count=6 unique=6 answer=zoo contains=true
count=6 unique=6 answer=ability contains=true

Notes

Ran on Flutter 3.32.0 per .fvmrc, Dart 3.8.0, after flutter pub get and dart run build_runner build. flutter analyze reports the same single pre-existing use_build_context_synchronously info in unconfirmed_transaction.dart before and after, nothing new.

One caveat on running the repro yourself: the view model imports reach lib/coins/litecoin/wallet.dart, which imports cw_mweb, so a host flutter test will not compile until cw_mweb/lib/generated_bindings.g.dart exists. I stubbed the three symbols mweb_ffi.dart references rather than build mwebd. Nothing on the path under test calls into them.

The scratch test is not part of this PR since the repo has no test/ directory. Happy to add it if you want one.

The pick-the-correct-word screen built its options by taking 5 words off
the top of a shuffled wordlist and then appending the answer, so whenever
the answer was already among those 5 it was listed twice. The toSet()
call meant to catch that threw its result away.

The shuffle also ran on the caller's list rather than a copy, which
permanently reordered Bip39.english for the rest of the session.
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