Descriptor Wallet Backup
Proposing BIP392: Encryption path to support descriptor wallet backup
The goal is to be able to backup more complex script wallets without changing the mnemonic seed interface for wallet management.
Just writing down your own mnemonic is not enough to recover scripts that have data outside of your own private key.
For example, time locks, hash locks or public keys of other parties.
This information lives outside the mnemonic and without it users cannot recover more complex script wallets.
We solve this issue by defining a new derivation path m/392 to generate encryption keys.
These keys will be used to encrypt a .txt file that contains all the external information to recover script wallets.
This .txt file can now be redundantly stored in multiple locations: GDrive, DropBox, you friends, family, everywhere you can think of.
This way, if you lose the device (mobile) managing your descriptor wallet, you can still recover the entire wallet with JUST your mnemonic.
Current script wallets like Muun and Hexa wallet, use a separate encryption key that the users are expected to write down alongside their seed. This creates a UI hurdle and takes away from the original interface of just remembering a single seed to recover funds.
@mocodesmo We will need your help with this since its our first time adding a feature to this repo.
Before we start this we have to note:
- This is only for script wallets. In our case inheritance.
- Script wallet creation flow needs updates first. It must accept making a script just using pub keys as a watch only.
- Script wallets also have the option to be in a
PartialorCompletestate. This allows us to start aPartialscript with our key management for the script ready; waiting for other external data (pub keys, timelocks, hash locks) which can be added later toCompletethe setup.
All this must happen before any interaction of knowledge of cypher post. Just as a regular bitcoin script wallet, that expects your to get all this information from the outside to make a script. Once we have this manual setup, then we can just plug in an external data source to make this wallet Complete.
model/wallet.dart main class can use String descriptor, instead of holding 3 different InternalWallet class as extra variables.
The descriptor will have information about the keys and whether it is watcher(pub-only) or spender(w/privkeys).
Also all the stackmate-core/wallet functions take mainly the descriptor as input.
There will also be an additional recoveryData section with public descriptor data.
@freezed
class Wallet with _$Wallet {
@HiveType(typeId: 1, adapterName: 'WalletClassAdaper')
const factory Wallet({
@HiveField(0) required String label,
@HiveField(1) required String descriptor,
@HiveField(1.3) required int recoveryDataRequired ,
@HiveField(1.3) required List<RecoveryData> recoveryData,
@HiveField(2) required String blockchain,
@HiveField(3) List<Transaction>? transactions,
@HiveField(4) int? id,
@HiveField(5) int? balance,
@HiveField(6) required String walletType,
// @Default(false) @HiveField(7) bool watchOnly,
}) = _Wallet;
const Wallet._();
So in cubit/new_wallet
instead of :
var newWallet = Wallet(
label: state.walletLabel,
walletType: 'SINGLE ACCOUNT',
mainWallet: InternalWallet(
xPub: wallet.xpub,
fingerPrint: wallet.fingerPrint,
path: wallet.hardenedPath,
descriptor: com.descriptor.split('#')[0],
),
exportWallet: InternalWallet(
xPub: exportWallet.xpub,
fingerPrint: exportWallet.fingerPrint,
path: exportWallet.hardenedPath,
),
blockchain: _blockchainCubit.state.blockchain.name,
);
We can just do:
var newWallet = Wallet(
label: state.walletLabel,
walletType: 'SINGLE ACCOUNT',
descriptor: privCom.descriptor,
recoveryDataRequired: 1,
recoveryData: [
RecoveryData(extendedKey(ExtendedKey(
xKey: wallet.xpub,
fingerPrint: wallet.fingerPrint,
path: wallet.hardenedPath,
id: random(32),
).toString())),],
blockchain: _blockchainCubit.state.blockchain.name,
watchOnly: isWatchOnly(privCom.descriptor)
);
a naive isWatchOnly just needs to look for an xprv string in the descriptor. This is fine for now since we only support extended key format in descriptors. If we decide to support normal ec key format, then we will have to update this. We can also get stackmate-core to return watchOnly.
Edit: privCom: Indicates that even in case of a private descriptor wallet, recoveryData values are always stored as public values. This makes every wallet, by default a watchOnly. If someone converts it to a spender into watch only just use the recoveryData to recreate the public descriptor.
Edit: recoveryDataRequired will indicate how many conditions of data are required for the entire wallet to be created/recovered.
and I think we should always show/accept the public key as the full extended public key [fingerprint/path]xpub, rather than separating them. Makes the UX more simple. Only copy/paste 1 thing, rather than 3.
and if a user is making a watch-only wallet, if they even paste the entire descriptor we should recognise it and recover the wallet based on that.
blue wallet has this recovery feature where they say just dump whatever you have and we will try out best to recover.
So one text area where you put whatever you have and create a wallet if possible with that data.
@freezed
class ExtendedKey with _$ExtendedKey {
@HiveType(typeId: 2, adapterName: 'ExtendedKeyClassAdaper')
const factory ExtendedKey({
@HiveField(0) required String xKey,
@HiveField(1) required String fingerPrint,
@HiveField(2) required String path,
@HiveField(3) required String id,
}) = _ExtendedKey;
const ExtendedKey._();
factory ExtendedKey.fromJson(Map<String, dynamic> json) =>
_$ExtendedKeyFromJson(json);
factory ExtendedKey.toString()...
factory ExtendedKey.fromString()...
}
This class was InternalWallet but it is technically ExtendedKey. It mainly deals with the results from stackmate-core key module. Once the right keys go into policy.compile - the output descriptor is all that is needed to use the wallet module.
Edit: removed @HiveField(4) required String? descriptor. Descriptor is now in Wallet and has the relevant keys within it to operate. So Wallet does not need to hold an ExtendedKey, just a descriptor made from one or more ExtendedKey.
Under Info we will now show the full extended pub key for single sig wallets and for script wallets, we will show the public descriptor and allow you to encrypt and share that everywhere.
We can add other info for scripts like a policy breakdown. The policy parts and the satisfaction threshold.
And inheritance only has a flow for recovering as the primary signer. we need to include a flow for secondary signer.
Also we should have a page before we start that explains the script. Shows the 2 parts, introduces it as 1/2. Then asks you to setup each part. You can leave the setup half way, and finish it later.
So for some duration we will have to store ExtendedKey as part of Wallet. So we could have an array of keys as a variable in Wallet along side descriptor.
enum RecoveryData{
extendedKey('[fingerprint/path]xkey'),
timeLock('21000000'),
hashLock('9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08');
const RecoveryData(this.value);
final String value;
}
Edit: Updated Wallet.recoveryData to be a List<RecoveryData>.
Latest:
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';
import 'package:sats/model/transaction.dart';
part 'wallet.g.dart';
part 'wallet.freezed.dart';
const satsInBTC = 100000000;
@freezed
class Wallet with _$Wallet {
@HiveType(typeId: 1, adapterName: 'WalletClassAdaper')
const factory Wallet({
@HiveField(0) required String label,
@HiveField(1) required String descriptor,
@HiveField(1) required String policy,
@HiveField(2) required int requiredPolicyElements,
@HiveField(2) required List<policyElement> policyElements,
@HiveField(4) required String blockchain,
@HiveField(5) List<Transaction>? transactions,
@HiveField(6) int? id,
@HiveField(7) int? balance,
@HiveField(8) required String walletType,
// @Default(false) @HiveField(7) bool watchOnly,
}) = _Wallet;
const Wallet._();
factory Wallet.fromJson(Map<String, dynamic> json) => _$WalletFromJson(json);
String balanceToBtc() =>
balance == null ? '0' : (balance! / satsInBTC).toStringAsFixed(8);
bool isNotWatchOnly() => label != 'WATCH ONLY';
}
// Example Policy:
// or( pk(___main___) , and(after(___exit-timelock___),hash160(___exit-password___)) )
enum policyElement {
publicKey("[fingerprint/path]xkey","main"),
timeLock("7894564","exit-timelock"),
hashLock("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08","exit-password");
const policyElement(this.value, this.identifier);
final String value;
final String identifier;
}
I think this is more clear and extendible.
policy is a readable format of the contract which the UI can use to show it in a friendly way.
Another addition to backup is encrypted single sig seed with passphrase, exported to SD Card ONLY!