Merge #1399: Template multisig security

36af35fae3f21995fcbe0c4c1a5f685a2318d0a0 refac: remove select button at the choose template step (edouardparis)
2eddbbd7000ccb8e392a3da4fd854651bf604a80 change wording in inheritance template (edouardparis)
789c2aebfb3731b43b9d1750d5586db1456c14fc Add new template: multisig security wallet (edouardparis)

Pull request description:

  ![20241029_18h22m22s_grim](https://github.com/user-attachments/assets/e87daab2-82bc-496e-bfc1-98feb9c03731)
  ![20241029_18h22m31s_grim](https://github.com/user-attachments/assets/d2e54c4d-c76b-48b2-95ae-f9482af0284d)

ACKs for top commit:
  edouardparis:
    Self-ACK 36af35fae3f21995fcbe0c4c1a5f685a2318d0a0

Tree-SHA512: 4e6617adb2eb7f0932aca8260f6fad120dcdb2aafc2c0880036bddc31874b1ba4280dbb27eb495d601a10948fb3aaf40ca4c5fd8922fc8660fc269a4c80a5cff
This commit is contained in:
edouardparis 2024-10-30 14:39:41 +01:00
commit 2d5991cb0c
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
11 changed files with 470 additions and 98 deletions

View File

@ -50,6 +50,7 @@ pub enum DescriptorTemplate {
#[default]
SimpleInheritance,
Custom,
MultisigSecurity,
}
#[derive(Clone)]

View File

@ -113,6 +113,8 @@ pub enum InternalBitcoindMsg {
pub enum DefineDescriptor {
ChangeTemplate(context::DescriptorTemplate),
ImportDescriptor(String),
KeysEdited(Vec<(usize, usize)>, Key),
KeysEdit(Vec<(usize, usize)>),
Path(usize, DefinePath),
AddRecoveryPath,
KeyModal(ImportKeyModal),
@ -136,7 +138,6 @@ pub enum DefineKey {
Delete,
Edit,
Clipboard(String),
Edited(Key),
}
#[derive(Debug, Clone)]

View File

@ -74,8 +74,7 @@ pub fn check_key_network(key: &DescriptorPublicKey, network: Network) -> bool {
pub struct EditXpubModal {
device_must_support_tapminiscript: bool,
path_index: usize,
key_index: usize,
keys_coordinate: Vec<(usize, usize)>,
network: Network,
error: Option<Error>,
processing: bool,
@ -99,8 +98,7 @@ impl EditXpubModal {
device_must_support_tapminiscript: bool,
other_path_keys: HashSet<Fingerprint>,
key: Option<Key>,
path_index: usize,
key_index: usize,
keys_coordinate: Vec<(usize, usize)>,
network: Network,
hot_signer: Arc<Mutex<Signer>>,
hot_signer_fingerprint: Fingerprint,
@ -128,8 +126,7 @@ impl EditXpubModal {
},
manually_imported_xpub,
keys,
path_index,
key_index,
keys_coordinate,
processing: false,
error: None,
network,
@ -304,22 +301,15 @@ impl super::DescriptorEditModal for EditXpubModal {
}
message::ImportKeyModal::ConfirmXpub => {
if let Some(mut key) = self.chosen_signer.clone() {
let key_index = self.key_index;
key.name.clone_from(&self.form_name.value);
if self.other_path_keys.contains(&key.fingerprint) {
self.duplicate_master_fg = true;
} else {
let path_index = self.path_index;
let coordinate = self.keys_coordinate.clone();
return Command::perform(
async move { (path_index, key_index, key) },
move |(path_index, key_index, key)| {
message::DefineDescriptor::Path(
path_index,
message::DefinePath::Key(
key_index,
message::DefineKey::Edited(key),
),
)
async move { (coordinate, key) },
move |(coordinate, key)| {
message::DefineDescriptor::KeysEdited(coordinate, key)
},
)
.map(Message::DefineDescriptor);
@ -346,11 +336,7 @@ impl super::DescriptorEditModal for EditXpubModal {
fn view<'a>(&'a self, hws: &'a HardwareWallets) -> Element<'a, Message> {
let chosen_signer = self.chosen_signer.as_ref().map(|s| s.fingerprint);
view::editor::edit_key_modal(
if self.path_index > 0 {
"Set your key"
} else {
"Set your primary key"
},
"Set your key",
self.network,
hws.list
.iter()

View File

@ -74,6 +74,23 @@ impl Path {
}
}
pub fn with_n_keys(mut self, n: usize) -> Self {
self.keys = Vec::new();
for _i in 0..n {
self.keys.push(None);
}
self
}
pub fn with_threshold(mut self, t: usize) -> Self {
self.threshold = if t > self.keys.len() {
self.keys.len()
} else {
t
};
self
}
fn valid(&self) -> bool {
!self.keys.is_empty() && !self.keys.iter().any(|k| k.is_none()) && !self.duplicate_sequence
}
@ -164,6 +181,12 @@ impl DefineDescriptor {
DescriptorTemplate::SimpleInheritance => {
self.paths = vec![Path::new_primary_path(), Path::new_recovery_path()];
}
DescriptorTemplate::MultisigSecurity => {
self.paths = vec![
Path::new_primary_path().with_n_keys(2).with_threshold(2),
Path::new_recovery_path().with_n_keys(3).with_threshold(2),
];
}
DescriptorTemplate::Custom => {
self.paths = vec![Path::new_primary_path(), Path::new_recovery_path()];
}
@ -195,6 +218,46 @@ impl Step for DefineDescriptor {
Message::DefineDescriptor(message::DefineDescriptor::AddRecoveryPath) => {
self.paths.push(Path::new_recovery_path());
}
Message::DefineDescriptor(message::DefineDescriptor::KeysEdited(coordinate, key)) => {
hws.set_alias(key.fingerprint, key.name.clone());
for (i, j) in coordinate {
self.paths[i].keys[j] = Some(key.fingerprint);
}
self.keys.insert(key.fingerprint, key);
self.modal = None;
self.check_setup();
}
Message::DefineDescriptor(message::DefineDescriptor::KeysEdit(coordinate)) => {
let use_taproot = self.use_taproot;
let mut set = HashSet::<Fingerprint>::new();
let key = coordinate
.first()
.and_then(|(i, j)| self.paths[*i].keys[*j])
.and_then(|f| self.keys.get(&f))
.cloned();
for (i, j) in &coordinate {
set.extend(self.paths[*i].keys.iter().filter_map(|key| {
if key.is_some() && key != &self.paths[*i].keys[*j] {
*key
} else {
None
}
}));
}
let modal = EditXpubModal::new(
use_taproot,
set,
key,
coordinate,
self.network,
self.signer.clone(),
self.signer_fingerprint,
self.keys.values().cloned().collect(),
);
let cmd = modal.load();
self.modal = Some(Box::new(modal));
return cmd;
}
Message::DefineDescriptor(message::DefineDescriptor::Path(i, msg)) => match msg {
message::DefinePath::SequenceEdited(seq) => {
self.modal = None;
@ -233,13 +296,7 @@ impl Step for DefineDescriptor {
message::DefineKey::Clipboard(key) => {
return Command::perform(async move { key }, Message::Clibpboard);
}
message::DefineKey::Edited(key) => {
hws.set_alias(key.fingerprint, key.name.clone());
self.paths[i].keys[j] = Some(key.fingerprint);
self.keys.insert(key.fingerprint, key);
self.modal = None;
self.check_setup();
}
message::DefineKey::Edit => {
let use_taproot = self.use_taproot;
let path = &self.paths[i];
@ -253,8 +310,7 @@ impl Step for DefineDescriptor {
}
})),
path.keys[j].and_then(|f| self.keys.get(&f)).cloned(),
i,
j,
vec![(i, j)],
self.network,
self.signer.clone(),
self.signer_fingerprint,
@ -424,6 +480,17 @@ impl Step for DefineDescriptor {
self.valid(),
)
}
DescriptorTemplate::MultisigSecurity => {
view::editor::template::multisig_security_wallet::multisig_security_template(
progress,
self.use_taproot,
self.path_keys(&self.paths[0]),
self.path_keys(&self.paths[1]),
self.paths[1].sequence,
self.paths[1].threshold,
self.valid(),
)
}
DescriptorTemplate::Custom => view::editor::template::custom::custom_template(
progress,
self.use_taproot,
@ -714,25 +781,21 @@ mod tests {
sandbox.load(&ctx).await;
let key = DescriptorPublicKey::from_str("[4df3f0e3/84'/0'/0']tpubDDRs9DnRUiJc4hq92PSJKhfzQBgHJUrDo7T2i48smsDfLsQcm3Vh7JhuGqJv8zozVkNFin8YPgpmn2NWNmpRaE3GW2pSxbmAzYf2juy7LeW").unwrap();
let specter_key = message::DefinePath::Key(
0,
message::DefineKey::Edited(Key {
name: "My Specter key".to_string(),
fingerprint: key.master_fingerprint(),
key,
device_kind: Some(async_hwi::DeviceKind::Specter),
device_version: None,
is_compatible_taproot: false,
is_hot_signer: false,
}),
);
let specter_key = Key {
name: "My Specter key".to_string(),
fingerprint: key.master_fingerprint(),
key,
device_kind: Some(async_hwi::DeviceKind::Specter),
device_version: None,
is_compatible_taproot: false,
is_hot_signer: false,
};
// Use Specter device for primary key
sandbox
.update(Message::DefineDescriptor(message::DefineDescriptor::Path(
0,
specter_key.clone(),
)))
.update(Message::DefineDescriptor(
message::DefineDescriptor::KeysEdited(vec![(0, 0)], specter_key.clone()),
))
.await;
// Edit recovery key
@ -795,10 +858,9 @@ mod tests {
// Now edit the recovery key to use Specter device
sandbox
.update(Message::DefineDescriptor(message::DefineDescriptor::Path(
1,
specter_key.clone(),
)))
.update(Message::DefineDescriptor(
message::DefineDescriptor::KeysEdited(vec![(1, 0)], specter_key.clone()),
))
.await;
sandbox.check(|step| {
assert!((step).apply(&mut ctx));

View File

@ -73,7 +73,10 @@ impl Step for DescriptorTemplateDescription {
DescriptorTemplate::SimpleInheritance => {
view::editor::template::inheritance::inheritance_template_description(progress)
}
DescriptorTemplate::Custom { .. } => {
DescriptorTemplate::MultisigSecurity => {
view::editor::template::multisig_security_wallet::multisig_security_template_description(progress)
}
DescriptorTemplate::Custom => {
view::editor::template::custom::custom_template_description(progress)
}
}

View File

@ -6,6 +6,7 @@ use iced::{Alignment, Length};
use liana::miniscript::bitcoin::Network;
use liana_ui::component::text::{self, h3, p1_bold, p2_regular, H3_SIZE};
use liana_ui::image;
use std::borrow::Cow;
use std::str::FromStr;
use liana::miniscript::bitcoin::{self, bip32::Fingerprint};
@ -130,7 +131,7 @@ pub fn path(
pub fn defined_key<'a>(
alias: &'a str,
color: iced::Color,
title: &'static str,
title: impl Into<Cow<'a, str>>,
warning: Option<&'static str>,
fixed: bool,
) -> Element<'a, message::DefineKey> {
@ -177,7 +178,7 @@ pub fn defined_key<'a>(
pub fn undefined_key<'a>(
color: iced::Color,
title: &'static str,
title: impl Into<Cow<'a, str>>,
active: bool,
fixed: bool,
) -> Element<'a, message::DefineKey> {

View File

@ -4,7 +4,7 @@ use liana_ui::{
color,
component::{
button, collapse,
text::{h3, p1_regular, text, Text},
text::{h3, p1_regular, text, Text, H3_SIZE},
},
icon, image, theme,
widget::*,
@ -30,16 +30,32 @@ pub fn inheritance_template_description(progress: (usize, usize)) -> Element<'st
.push(h3("Inheritance wallet"))
.max_width(800.0)
.push(Container::new(
p1_regular("For this Inheritance wallet you will need 2 Keys: Your Primary Key and an Inheritance Key to be given to a chosen heir. For security reasons, we suggest you use 2 Hardware Wallets to store them.")
p1_regular("For this Inheritance wallet you will need 2 Keys: Your Primary Key (for yourself) and an Inheritance Key (for your heir). For security reasons, we suggest you use 2 Hardware Wallets to store them.")
.style(color::GREY_2)
.horizontal_alignment(alignment::Horizontal::Left)
).align_x(alignment::Horizontal::Left).width(Length::Fill))
.push(Row::new()
.spacing(30)
.push(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(icon::round_key_icon().size(H3_SIZE).style(color::GREEN))
.push(p1_regular("Primary key").bold())
).push(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(icon::round_key_icon().size(H3_SIZE).style(color::WHITE))
.push(p1_regular("Inheritance key").bold())
))
.push(Container::new(
p1_regular("You will always be able to spend using your Primary Key.
After a period of inactivity (but not before that) your Inheritance Key will become able to recover your funds. Give it to your heir(s) in order to be able to collect their inheritance.")
.style(color::GREY_2)
.horizontal_alignment(alignment::Horizontal::Left)
).align_x(alignment::Horizontal::Left).width(Length::Fill))
.push(image::inheritance_template_description().width(Length::Fill))
.push(Container::new(
p1_regular("Your relatives Inheritance Key will become active only if you dont move the coins in your wallet for the defined period of time, enabling him/her to recover your funds while not being able to access them before that.")
.style(color::GREY_2)
.horizontal_alignment(alignment::Horizontal::Left)
).align_x(alignment::Horizontal::Left).width(Length::Fill))
.push(Row::new().push(Space::with_width(Length::Fill)).push(button::primary(None, "Select").width(Length::Fixed(200.0)).on_press(Message::Next)))
.spacing(20),
true,

View File

@ -1,14 +1,13 @@
pub mod custom;
pub mod inheritance;
pub mod multisig_security_wallet;
use iced::{alignment, Alignment, Length};
use liana_ui::{
color,
component::{
button, card,
text::{h3, p1_regular, p2_regular},
},
component::text::{h3, p1_regular, p2_regular},
theme,
widget::*,
};
@ -25,45 +24,55 @@ pub fn choose_descriptor_template(progress: (usize, usize)) -> Element<'static,
.align_items(Alignment::Start)
.push(Container::new(
p1_regular("What do you want your wallet for? This depends on the amount of funds you have, the more funds, the higher the security should be. Not sure about the wallet type? We can help you.")
.style(color::GREY_2)
.horizontal_alignment(alignment::Horizontal::Left)
.style(color::GREY_2)
.horizontal_alignment(alignment::Horizontal::Left)
).align_x(alignment::Horizontal::Left).width(Length::Fill))
.push(
card::simple(
Row::new()
.align_items(Alignment::Center)
.push(
Column::new()
.align_items(Alignment::Start)
.push(h3("Simple inheritance"))
.push(p2_regular("Two keys required, one for yourself to spend and another for your heir.").style(color::GREY_2))
.width(Length::Fill)
)
.push(button::secondary(None, "Select").on_press(
Message::SelectDescriptorTemplate(
context::DescriptorTemplate::SimpleInheritance,
),
)),
Button::new(
Column::new()
.align_items(Alignment::Start)
.push(h3("Simple inheritance"))
.push(p2_regular("Two keys required, one for yourself to spend and another for your heir.").style(color::GREY_2))
.width(Length::Fill)
)
.padding(15)
.on_press(
Message::SelectDescriptorTemplate(
context::DescriptorTemplate::SimpleInheritance,
)
).style(theme::Button::Secondary)
.width(Length::Fill),
)
.push(
card::simple(
Row::new()
.align_items(Alignment::Center)
.push(
Column::new()
.align_items(Alignment::Start)
.push(h3("Custom (choose your own)"))
.push(p2_regular("Create a custom setup that fits all your needs").style(color::GREY_2))
.width(Length::Fill)
)
.push(button::secondary(None, "Select").on_press(
Message::SelectDescriptorTemplate(
context::DescriptorTemplate::Custom ,
),
)),
Button::new(
Column::new()
.align_items(Alignment::Start)
.push(h3("Multisig security wallet"))
.push(p2_regular("A secure scheme requiring stricter multiparty signature and recovery.").style(color::GREY_2))
.width(Length::Fill)
)
.padding(15)
.on_press(
Message::SelectDescriptorTemplate(
context::DescriptorTemplate::MultisigSecurity,
)
).style(theme::Button::Secondary)
.width(Length::Fill),
)
.push(
Button::new(
Column::new()
.align_items(Alignment::Start)
.push(h3("Custom (choose your own)"))
.push(p2_regular("Create a custom setup that fits all your needs.").style(color::GREY_2))
.width(Length::Fill)
)
.padding(15)
.on_press(
Message::SelectDescriptorTemplate(
context::DescriptorTemplate::Custom,
)
).style(theme::Button::Secondary)
.width(Length::Fill),
)
.spacing(20),

View File

@ -0,0 +1,240 @@
use iced::{alignment, widget::Space, Alignment, Length};
use liana_ui::{
color,
component::{
button, collapse,
text::{h3, p1_regular, text, Text, H3_SIZE},
},
icon, image, theme,
widget::*,
};
use crate::installer::{
context,
message::{self, Message},
step::descriptor::editor::key::Key,
view::{
editor::{define_descriptor_advanced_settings, defined_key, path, undefined_key},
layout,
},
};
pub fn multisig_security_template_description(
progress: (usize, usize),
) -> Element<'static, Message> {
layout(
progress,
None,
"Introduction",
Column::new()
.align_items(Alignment::Start)
.push(h3("Multisig security wallet"))
.max_width(800.0)
.push(Container::new(
p1_regular("For this setup you will need 3 keys: two Primary Keys and a Recovery Key. For security reasons, we suggest you use 3 Hardware Wallets to store them.")
.style(color::GREY_2)
.horizontal_alignment(alignment::Horizontal::Left)
).align_x(alignment::Horizontal::Left).width(Length::Fill))
.push(Row::new()
.spacing(30)
.push(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(icon::round_key_icon().size(H3_SIZE).style(color::GREEN))
.push(p1_regular("Primary key #1").bold())
).push(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(icon::round_key_icon().size(H3_SIZE).style(color::GREEN))
.push(p1_regular("Primary key #2").bold())
).push(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(icon::round_key_icon().size(H3_SIZE).style(color::ORANGE))
.push(p1_regular("Recovery key").bold())
))
.push(Container::new(
p1_regular("The Primary Keys will compose a 2-of-2 multisig which will be your always-active spending policy. In case one of your keys becomes unavailable, after a period of inactivity you will be able to recover your funds using the Recovery Key together with one of your Primary Keys (2-of-3 multisig):")
.style(color::GREY_2)
.horizontal_alignment(alignment::Horizontal::Left)
).align_x(alignment::Horizontal::Left).width(Length::Fill))
.push(image::multisig_security_template_description().width(Length::Fill))
.push(Row::new().push(Space::with_width(Length::Fill)).push(button::primary(None, "Select").width(Length::Fixed(200.0)).on_press(Message::Next)))
.spacing(20),
true,
Some(Message::Previous),
)
}
pub fn multisig_security_template<'a>(
progress: (usize, usize),
use_taproot: bool,
primary_keys: Vec<Option<&'a Key>>,
recovery_keys: Vec<Option<&'a Key>>,
sequence: u16,
threshold: usize,
valid: bool,
) -> Element<'a, Message> {
layout(
progress,
None,
"Set keys",
Column::new()
.align_items(Alignment::Start)
.max_width(1000.0)
.push(collapse::Collapse::new(
|| {
Button::new(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(text("Advanced settings").small().bold())
.push(icon::collapse_icon()),
)
.style(theme::Button::Transparent)
},
|| {
Button::new(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(text("Advanced settings").small().bold())
.push(icon::collapsed_icon()),
)
.style(theme::Button::Transparent)
},
move || define_descriptor_advanced_settings(use_taproot),
))
.push(
path(
color::GREEN,
None,
0,
false,
primary_keys.len(),
primary_keys
.iter()
.enumerate()
.map(|(i, primary_key)| {
if let Some(key) = primary_key {
defined_key(
&key.name,
color::GREEN,
format!("Primary key #{}", i + 1),
if use_taproot && !key.is_compatible_taproot {
Some("Key is not compatible with taproot")
} else {
None
},
true,
)
} else {
undefined_key(
color::GREEN,
format!("Primary key #{}", i + 1),
!primary_keys[0..i].iter().any(|k| k.is_none()),
true,
)
}
.map(move |msg| message::DefinePath::Key(i, msg))
})
.collect(),
true,
)
.map(move |msg| {
if let message::DefinePath::Key(i, message::DefineKey::Edit) = msg {
Message::DefineDescriptor(message::DefineDescriptor::KeysEdit(vec![
(0, i),
(1, i),
]))
} else {
Message::DefineDescriptor(message::DefineDescriptor::Path(0, msg))
}
}),
)
.push(
path(
color::ORANGE,
None,
sequence,
false,
threshold,
recovery_keys
.iter()
.enumerate()
.map(|(j, recovery_key)| {
if let Some(key) = recovery_key {
defined_key(
&key.name,
if j < 2 { color::GREEN } else { color::ORANGE },
if j < 2 {
format!("Primary key #{}", j + 1)
} else {
"Recovery key".to_string()
},
if use_taproot && !key.is_compatible_taproot {
Some("Key is not compatible with Taproot")
} else {
None
},
true,
)
} else {
undefined_key(
if j < 2 { color::GREEN } else { color::ORANGE },
if j < 2 {
format!("Primary key #{}", j + 1)
} else {
"Recovery key".to_string()
},
!(primary_keys.iter().any(|k| k.is_none())
|| recovery_keys[0..j].iter().any(|k| k.is_none())),
true,
)
}
.map(move |msg| message::DefinePath::Key(j, msg))
})
.collect(),
true,
)
.map(move |msg| {
if let message::DefinePath::Key(i, message::DefineKey::Edit) = msg {
Message::DefineDescriptor(message::DefineDescriptor::KeysEdit(if i < 2 {
vec![(0, i), (1, i)]
} else {
// recovery path is the path with three keys
vec![(1, i)]
}))
} else {
Message::DefineDescriptor(message::DefineDescriptor::Path(1, msg))
}
}),
)
.push(
Row::new()
.push(
button::secondary(None, "Customize")
.width(Length::Fixed(200.0))
.on_press(Message::DefineDescriptor(
message::DefineDescriptor::ChangeTemplate(
context::DescriptorTemplate::Custom,
),
)),
)
.push(Space::with_width(Length::Fill))
.push(
button::primary(None, "Continue")
.width(Length::Fixed(200.0))
.on_press_maybe(if valid { Some(Message::Next) } else { None }),
),
)
.push(Space::with_height(100.0))
.spacing(20),
true,
Some(Message::Previous),
)
}

View File

@ -75,3 +75,11 @@ pub fn custom_template_description() -> Svg {
let h = Handle::from_memory(CUSTOM_TEMPLATE_DESC.to_vec());
Svg::new(h)
}
const MULTISIG_SECURITY_TEMPLATE_DESC: &[u8] =
include_bytes!("../static/images/multisig_security_template.svg");
pub fn multisig_security_template_description() -> Svg {
let h = Handle::from_memory(MULTISIG_SECURITY_TEMPLATE_DESC.to_vec());
Svg::new(h)
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 173 KiB