Add SLIP-39 mnemonic and seed support
Adds SLIP-39 (Shamir's Secret Sharing) mnemonic and seed support to python-hdwallet:
- Implements SLIP39 mnemonic generation and validation with multi-share threshold schemes
- Adds SLIP39 entropy handling and seed derivation functionality
- Adds Nix development environment setup for reproducible builds
- Comprehensive test coverage for all new SLIP39 functionality
Benefits
- Enables secure multi-party wallet recovery through threshold secret sharing
- Maintains backward compatibility with existing BIP39 workflows
- Provides standardized SLIP39 implementation following official specifications
- Improves development workflow with Nix environment automation
Test Plan
- All existing tests pass
- New SLIP39-specific test suites cover mnemonic generation, validation, and seed derivation
- CLI integration tests verify SLIP39 functionality end-to-end
Sorry, @axiom90! I pushed another enhancement to the SLIP39Mnemonic.decode and .decode, to support human-readable SLIP-39 Mnemonics via tabulate. It's difficult for humans to reliably parse these huge sets of Mnemonics, so this gives output in a readable Human form, that also parses reliably for recovery.
OK, thanks!
There are still issues with the way Mnemonics are handled; BIP-39 mnemonics with accents (such as 'déclarer') are not handled correctly. Also, determining language from a set of mnemonics is not deterministic.
I'm working through these issues right now, so don't merge this yet...
OK, @meherett @axiom90 -- this is ready to take a look at.
The fundamental problem, is that Mnemonic languages are not deterministic, especially when accents and other letter UTF-8 "Marks" are ignored; generally required by all Mnemonic parsers. And, when abbreviations are allowed, it's even worse.
Thus, it is quite easy to come up with entropy that decodes to a Mnemonic phrase that is valid in english AND french; if you then decode that Mnemonic to try to recover the entropy (you originally asked for the french Mnemonic), you can end up with the wrong seed (decoded as English)!
So, I've improved the mnemonic APIs to support a preferred language for decoding the Mnemonic. This leaked into many parts of the code, unfortunately.
As part of this fix -- I've also implemented hdwallet.imnemonic.WordIndices; a Trie-based Mnemonic decoder that supports abbreviations and optional accents. It looks a lot like a simple {'word':
This allowed me to implement IMnemonic.collect -- you give it a list of languages, and it produces a Generator that computes the remaining language(s) that are possible, and all of the "Next" characters that are valid for the mnemonic word in those languages. See tests/test_bip39_cross_language.py, test_bip39_collection.
Anyway, take a look. This pull might need some more documentation updates before acceptance.
Well, I found another pretty much intractable issue with the current implementation of Mnemonic language deduction and decoding.
You just cannot separate langage deduction from decoding.
-
The IMnemonic.find_language interface is purely "statistical", guessing whether a Mnemonic looks like english, french, etc. Unfortunately (and handling abbreviations and optional UTF-8 Marks made this worse), many mnemonics "look" valid in multiple languages, even if they are not.
-
It is possible to end up generating Mnemonics that are completely valid in multiple languages, and also pass their internal "checksum" validation. These can only be detected when IMnemonic.decode is run, so that is where this exception has to be raised, not in .find_language.
So, this now implements full multi-language IMnemonic.decode across all of the implemented mnemonic types. If multiple languages might look valid, the .decode will attempt them all, and ensure that exactly one language correctly decodes the Mnemonic, failing if none do, or if multiple languages correctly decode it -- unless a preferred language is specified.
Improved the validation of ISeed implementations, particularly around Cardano. There were many instances of incorrect CardanoSeed handling, which did not identify the 'cardano_type' and thus defaulted to different Cardano seed types.