Skip to content

Commit 9b60367

Browse files
committed
[WIP] feat(rust): add Miniscript support
1 parent 7d12bc1 commit 9b60367

10 files changed

Lines changed: 496 additions & 356 deletions

File tree

rust/trezor-client/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/trezor-client/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ bitcoin = { version = "0.32", optional = true }
5252
unicode-normalization = { version = "0.1.22", optional = true }
5353

5454
[dev-dependencies]
55-
tracing-subscriber = "0.3"
55+
base64 = "0.22"
5656
serial_test = "3"
57+
tracing-subscriber = "0.3"
5758

5859
[features]
5960
default = ["bitcoin", "ethereum", "solana"]

rust/trezor-client/examples/interaction.rs

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,20 @@
1-
use std::io;
2-
31
use bitcoin::{bip32, network::Network, Address};
4-
use trezor_client::{Error, TrezorMessage, TrezorResponse};
5-
6-
fn handle_interaction<T, R: TrezorMessage>(resp: TrezorResponse<T, R>) -> Result<T, Error> {
7-
match resp {
8-
TrezorResponse::Ok(res) => Ok(res),
9-
TrezorResponse::Failure(_) => resp.ok(), // assering ok() returns the failure error
10-
TrezorResponse::ButtonRequest(req) => handle_interaction(req.ack()?),
11-
TrezorResponse::PinMatrixRequest(req) => {
12-
println!("Enter PIN");
13-
let mut pin = String::new();
14-
if io::stdin().read_line(&mut pin).unwrap() != 5 {
15-
println!("must enter pin, received: {}", pin);
16-
}
17-
// trim newline
18-
handle_interaction(req.ack_pin(pin[..4].to_owned())?)
19-
}
20-
TrezorResponse::PassphraseRequest(req) => {
21-
println!("Enter passphrase");
22-
let mut pass = String::new();
23-
io::stdin().read_line(&mut pass).unwrap();
24-
// trim newline
25-
handle_interaction(req.ack_passphrase(pass[..pass.len() - 1].to_owned())?)
26-
}
27-
}
28-
}
292

303
fn do_main() -> Result<(), trezor_client::Error> {
314
// init with debugging
325
let mut trezor = trezor_client::unique(true)?;
336
trezor.init_device(None)?;
347

35-
let xpub = handle_interaction(
36-
trezor.get_public_key(
37-
&vec![
38-
bip32::ChildNumber::from_hardened_idx(0).unwrap(),
39-
bip32::ChildNumber::from_hardened_idx(0).unwrap(),
40-
bip32::ChildNumber::from_hardened_idx(0).unwrap(),
41-
]
42-
.into(),
43-
trezor_client::protos::InputScriptType::SPENDADDRESS,
44-
Network::Testnet,
45-
true,
46-
)?,
8+
let xpub = trezor.get_public_key(
9+
&vec![
10+
bip32::ChildNumber::from_hardened_idx(0).unwrap(),
11+
bip32::ChildNumber::from_hardened_idx(0).unwrap(),
12+
bip32::ChildNumber::from_hardened_idx(0).unwrap(),
13+
]
14+
.into(),
15+
trezor_client::protos::InputScriptType::SPENDADDRESS,
16+
Network::Testnet,
17+
true,
4718
)?;
4819
println!("{}", xpub);
4920
println!("{:?}", xpub);

rust/trezor-client/examples/sign_message.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,14 @@ fn main() {
1010
let mut trezor = trezor_client::unique(false).unwrap();
1111
trezor.init_device(None).unwrap();
1212

13-
let pubkey = handle_interaction(
14-
trezor
15-
.get_public_key(
16-
&DerivationPath::from_str("m/44h/1h/0h/0/0").unwrap(),
17-
trezor_client::protos::InputScriptType::SPENDADDRESS,
18-
Network::Testnet,
19-
true,
20-
)
21-
.unwrap(),
22-
)
23-
.unwrap();
13+
let pubkey = trezor
14+
.get_public_key(
15+
&DerivationPath::from_str("m/44h/1h/0h/0/0").unwrap(),
16+
trezor_client::protos::InputScriptType::SPENDADDRESS,
17+
Network::Testnet,
18+
true,
19+
)
20+
.unwrap();
2421
let addr = Address::p2pkh(pubkey.to_pub(), Network::Testnet);
2522
println!("address: {}", addr);
2623

Lines changed: 16 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,32 @@
1-
use std::io::{self, Write};
2-
31
use bitcoin::{
42
bip32, blockdata::script::Builder, consensus::encode::Decodable, network::Network, psbt,
53
transaction::Version, Address, Amount, Sequence, Transaction, TxIn, TxOut,
64
};
75

8-
use trezor_client::{Error, SignTxProgress, TrezorMessage, TrezorResponse};
9-
10-
fn handle_interaction<T, R: TrezorMessage>(resp: TrezorResponse<T, R>) -> T {
11-
match resp {
12-
TrezorResponse::Ok(res) => res,
13-
// assering ok() returns the failure error
14-
TrezorResponse::Failure(_) => resp.ok().unwrap(),
15-
TrezorResponse::ButtonRequest(req) => handle_interaction(req.ack().unwrap()),
16-
TrezorResponse::PinMatrixRequest(req) => {
17-
println!("Enter PIN");
18-
let mut pin = String::new();
19-
if io::stdin().read_line(&mut pin).unwrap() != 5 {
20-
println!("must enter pin, received: {}", pin);
21-
}
22-
// trim newline
23-
handle_interaction(req.ack_pin(pin[..4].to_owned()).unwrap())
24-
}
25-
TrezorResponse::PassphraseRequest(req) => {
26-
println!("Enter passphrase");
27-
let mut pass = String::new();
28-
io::stdin().read_line(&mut pass).unwrap();
29-
// trim newline
30-
handle_interaction(req.ack_passphrase(pass[..pass.len() - 1].to_owned()).unwrap())
31-
}
32-
}
33-
}
34-
35-
fn tx_progress(
36-
psbt: &mut psbt::Psbt,
37-
progress: SignTxProgress,
38-
raw_tx: &mut Vec<u8>,
39-
) -> Result<(), Error> {
40-
if let Some(part) = progress.get_serialized_tx_part() {
41-
raw_tx.write_all(part).unwrap();
42-
}
43-
44-
if !progress.finished() {
45-
let progress = handle_interaction(progress.ack_psbt(psbt, Network::Testnet).unwrap());
46-
tx_progress(psbt, progress, raw_tx)
47-
} else {
48-
Ok(())
49-
}
50-
}
51-
526
fn main() {
537
tracing_subscriber::fmt().with_max_level(tracing::Level::TRACE).init();
548

559
// init with debugging
5610
let mut trezor = trezor_client::unique(false).unwrap();
5711
trezor.init_device(None).unwrap();
5812

59-
let pubkey = handle_interaction(
60-
trezor
61-
.get_public_key(
62-
&vec![
63-
bip32::ChildNumber::from_hardened_idx(0).unwrap(),
64-
bip32::ChildNumber::from_hardened_idx(0).unwrap(),
65-
bip32::ChildNumber::from_hardened_idx(1).unwrap(),
66-
]
67-
.into(),
68-
trezor_client::protos::InputScriptType::SPENDADDRESS,
69-
Network::Testnet,
70-
true,
71-
)
72-
.unwrap(),
73-
);
13+
let pubkey = trezor
14+
.get_public_key(
15+
&vec![
16+
bip32::ChildNumber::from_hardened_idx(0).unwrap(),
17+
bip32::ChildNumber::from_hardened_idx(0).unwrap(),
18+
bip32::ChildNumber::from_hardened_idx(1).unwrap(),
19+
]
20+
.into(),
21+
trezor_client::protos::InputScriptType::SPENDADDRESS,
22+
Network::Testnet,
23+
true,
24+
)
25+
.unwrap();
7426
let addr = Address::p2pkh(pubkey.to_pub(), Network::Testnet);
7527
println!("address: {}", addr);
7628

77-
let mut psbt = psbt::Psbt {
29+
let psbt = psbt::Psbt {
7830
unsigned_tx: Transaction {
7931
version: Version::ONE,
8032
lock_time: bitcoin::absolute::LockTime::from_consensus(0),
@@ -111,9 +63,7 @@ fn main() {
11163
hex::encode(bitcoin::consensus::encode::serialize(&psbt.unsigned_tx))
11264
);
11365

114-
let mut raw_tx = Vec::new();
115-
let progress = handle_interaction(trezor.sign_tx(&psbt, Network::Testnet).unwrap());
116-
tx_progress(&mut psbt, progress, &mut raw_tx).unwrap();
66+
let signed = trezor.sign_tx(&psbt, Network::Testnet, None).unwrap();
11767

118-
println!("signed tx: {}", hex::encode(raw_tx));
68+
println!("signed tx: {}", hex::encode(signed.serialized));
11969
}

rust/trezor-client/src/client/bitcoin.rs

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::HashMap;
2+
13
use super::{Trezor, TrezorResponse};
24
use crate::{error::Result, flows::sign_tx::SignTxProgress, protos, utils};
35
use bitcoin::{
@@ -7,20 +9,51 @@ use bitcoin::{
79

810
pub use crate::protos::InputScriptType;
911

12+
#[derive(Default)]
13+
pub struct SignedTx {
14+
pub signatures: Vec<(usize, Vec<u8>)>,
15+
pub serialized: Vec<u8>,
16+
}
17+
1018
impl Trezor {
1119
pub fn get_public_key(
1220
&mut self,
1321
path: &bip32::DerivationPath,
1422
script_type: InputScriptType,
1523
network: Network,
1624
show_display: bool,
17-
) -> Result<TrezorResponse<'_, bip32::Xpub, protos::PublicKey>> {
25+
) -> Result<bip32::Xpub> {
1826
let mut req = protos::GetPublicKey::new();
1927
req.address_n = utils::convert_path(path);
2028
req.set_show_display(show_display);
2129
req.set_coin_name(utils::coin_name(network)?);
2230
req.set_script_type(script_type);
23-
self.call(req, Box::new(|_, m| Ok(m.xpub().parse()?)))
31+
self.call(req, Box::new(|_, m: protos::PublicKey| Ok(m.xpub().parse()?)))?.interact()
32+
}
33+
34+
pub fn get_root_fingerprint(&mut self) -> Result<bip32::Fingerprint> {
35+
let xpub = self.get_public_key(
36+
&bip32::DerivationPath::default(),
37+
InputScriptType::SPENDADDRESS,
38+
bitcoin::Network::Bitcoin,
39+
false,
40+
)?;
41+
Ok(xpub.fingerprint())
42+
}
43+
44+
pub fn register_policy(
45+
&mut self,
46+
name: String,
47+
template: String,
48+
xpubs: Vec<String>,
49+
network: Network,
50+
) -> Result<protos::RegisteredPolicy> {
51+
let mut req = protos::Policy::new();
52+
req.set_coin_name(utils::coin_name(network)?);
53+
req.set_name(name);
54+
req.set_template(template);
55+
req.xpubs = xpubs;
56+
self.call(req, Box::new(|_, m: protos::RegisteredPolicy| Ok(m)))?.interact()
2457
}
2558

2659
//TODO(stevenroose) multisig
@@ -30,28 +63,77 @@ impl Trezor {
3063
script_type: InputScriptType,
3164
network: Network,
3265
show_display: bool,
33-
) -> Result<TrezorResponse<'_, Address, protos::Address>> {
66+
registered: Option<protos::RegisteredPolicy>,
67+
) -> Result<Address> {
3468
let mut req = protos::GetAddress::new();
3569
req.address_n = utils::convert_path(path);
3670
req.set_coin_name(utils::coin_name(network)?);
3771
req.set_show_display(show_display);
3872
req.set_script_type(script_type);
39-
self.call(req, Box::new(|_, m| parse_address(m.address())))
73+
req.registered = registered.into();
74+
self.call(req, Box::new(|_, m: protos::Address| parse_address(m.address())))?.interact()
4075
}
4176

4277
pub fn sign_tx(
4378
&mut self,
4479
psbt: &psbt::Psbt,
4580
network: Network,
46-
) -> Result<TrezorResponse<'_, SignTxProgress<'_>, protos::TxRequest>> {
81+
registered: Option<protos::RegisteredPolicy>,
82+
) -> Result<SignedTx> {
83+
let root_fingerprint = self.get_root_fingerprint()?;
84+
85+
// Filter public keys that belong to the current wallet.
86+
// Public key derivation must be done before transaction signature process.
87+
let mut derivations = HashMap::new();
88+
for input in &psbt.inputs {
89+
for (pubkey, (fpr, path)) in &input.bip32_derivation {
90+
if *fpr != root_fingerprint {
91+
continue;
92+
}
93+
let derived_pubkey = self
94+
.get_public_key(&path, InputScriptType::SPENDADDRESS, network, false)?
95+
.public_key;
96+
if *pubkey == derived_pubkey {
97+
if derivations.insert(derived_pubkey, path.clone()).is_some() {
98+
return Err(crate::Error::InvalidPsbt(format!(
99+
"Duplicate public key: {}",
100+
derived_pubkey
101+
)));
102+
}
103+
}
104+
}
105+
}
106+
47107
let tx = &psbt.unsigned_tx;
48108
let mut req = protos::SignTx::new();
49109
req.set_inputs_count(tx.input.len() as u32);
50110
req.set_outputs_count(tx.output.len() as u32);
51111
req.set_coin_name(utils::coin_name(network)?);
52112
req.set_version(tx.version.0 as u32);
53113
req.set_lock_time(tx.lock_time.to_consensus_u32());
54-
self.call(req, Box::new(|c, m| Ok(SignTxProgress::new(c, m))))
114+
115+
if registered.is_some() {
116+
// TODO: tx serialization is not fully supported with Miniscript
117+
req.serialize = Some(false);
118+
}
119+
120+
let req = self.call(req, Box::new(move |_, m: protos::TxRequest| Ok(m)))?.interact()?;
121+
122+
let mut progress =
123+
SignTxProgress::new(self, req, registered, root_fingerprint, derivations);
124+
let mut signed = SignedTx::default();
125+
loop {
126+
if let Some(part) = progress.get_serialized_tx_part() {
127+
signed.serialized.extend(part);
128+
}
129+
if let Some((i, sig)) = progress.get_signature() {
130+
signed.signatures.push((i, sig.to_vec()));
131+
}
132+
if progress.finished() {
133+
return Ok(signed);
134+
}
135+
progress.ack_psbt(psbt, network)?;
136+
}
55137
}
56138

57139
pub fn sign_message(

rust/trezor-client/src/client/common.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub enum InteractionType {
3232
//TODO(stevenroose) should this be FnOnce and put in an FnBox?
3333
/// Function to be passed to the `Trezor.call` method to process the Trezor response message into a
3434
/// general-purpose type.
35-
pub type ResultHandler<'a, T, R> = dyn Fn(&'a mut Trezor, R) -> Result<T>;
35+
pub type ResultHandler<'a, T, R> = dyn FnOnce(&'a mut Trezor, R) -> Result<T>;
3636

3737
/// A button request message sent by the device.
3838
pub struct ButtonRequest<'a, T, R: TrezorMessage> {
@@ -213,6 +213,10 @@ impl<'a, T, R: TrezorMessage> TrezorResponse<'a, T, R> {
213213
}
214214
}
215215
}
216+
217+
pub fn interact(self) -> Result<T> {
218+
handle_interaction(self)
219+
}
216220
}
217221

218222
pub fn handle_interaction<T, R: TrezorMessage>(resp: TrezorResponse<'_, T, R>) -> Result<T> {

0 commit comments

Comments
 (0)