lwk_common 0.16.0

Liquid Wallet Kit - Common utilities
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
#![cfg_attr(not(test), deny(clippy::unwrap_used))]
#![warn(missing_docs)]

//! A crate containing common code used in multiple other crate in the workspace, such as:
//!
//!  * Utils to inspect a PSET: get the net effect of a PSET on a given wallet [`pset_balance()`], or get how many
//!    signatures are missing , and which signers should provide them [`pset_signatures()`].
//!  * [`Signer`] trait: contains the methods to be implemented by a signer such as signing a pset or
//!    returning an xpub
//!
//!  To avoid circular dependencies this crate must not depend on other crate of the workspace

mod address;
mod balance;
mod descriptor;
mod encrypt;
mod error;
mod fee;
mod keyorigin_xpub;
mod model;
mod network;
pub mod precision;
mod qr;
mod segwit;
mod signer;
mod store;

pub use crate::address::{Address, AddressParseError};
pub use crate::balance::{Balance, SignedBalance};
pub use crate::descriptor::{
    multisig_desc, singlesig_desc, Bip, DescriptorBlindingKey, InvalidBipVariant,
    InvalidBlindingKeyVariant, InvalidMultisigVariant, InvalidSinglesigVariant, Multisig,
    Singlesig,
};
pub use crate::encrypt::{
    cipher_from_key_bytes, decrypt_with_nonce_prefix, encrypt_with_deterministic_nonce,
    encrypt_with_random_nonce, EncryptError,
};
pub use crate::error::Error;
pub use crate::keyorigin_xpub::{keyorigin_xpub_from_str, InvalidKeyOriginXpub};
pub use crate::model::*;
pub use crate::network::Network;
pub use crate::precision::Precision;
pub use crate::qr::*;
pub use crate::segwit::is_provably_segwit;
#[cfg(feature = "amp0")]
pub use crate::signer::amp0::{Amp0Signer, Amp0SignerData};
pub use crate::signer::Signer;
pub use crate::store::{
    BoxError, DynStore, EncryptedStore, EncryptedStoreError, FakeStore, FileStore, MemoryStore,
    Store,
};
pub use fee::*;

/// A trait for async read/write operations used by hardware wallet connections
pub trait Stream {
    /// The error type returned by read and write operations
    type Error: std::error::Error + Send + Sync + 'static;

    /// Read data from the stream into the provided buffer
    fn read(&self, buf: &mut [u8])
        -> impl std::future::Future<Output = Result<usize, Self::Error>>;
    /// Write data to the stream
    fn write(&self, data: &[u8]) -> impl std::future::Future<Output = Result<(), Self::Error>>;
}

use elements::confidential::{Asset, Value};
use elements_miniscript::confidential::bare::tweak_private_key;
use elements_miniscript::confidential::Key;
use elements_miniscript::descriptor::DescriptorSecretKey;
use elements_miniscript::elements::bitcoin::secp256k1::SecretKey;
use elements_miniscript::elements::{
    bitcoin::{bip32::KeySource, key::PublicKey},
    opcodes::all::OP_RETURN,
    pset::PartiallySignedTransaction,
    script::Builder,
    secp256k1_zkp::{All, Generator, PedersenCommitment, Secp256k1},
    AssetId, BlindAssetProofs, BlindValueProofs, OutPoint, Script, TxOutSecrets,
};
use elements_miniscript::{ConfidentialDescriptor, DescriptorPublicKey};
use std::collections::btree_map::BTreeMap;

/// The sockets of the Blockstream Liquid Electrum servers.
pub mod electrum_ssl {
    /// The socket of the Blockstream Liquid mainnet Electrum server.
    pub const LIQUID_SOCKET: &str = "elements-mainnet.blockstream.info:50002";
    /// The socket of the Blockstream Liquid testnet Electrum server.
    pub const LIQUID_TESTNET_SOCKET: &str = "elements-testnet.blockstream.info:50002";
}

/// Derive the script pubkey from a confidential descriptor and an index.
pub fn derive_script_pubkey(
    descriptor: &ConfidentialDescriptor<DescriptorPublicKey>,
    index: u32,
) -> Result<Script, Error> {
    Ok(descriptor
        .descriptor
        .at_derivation_index(index)?
        .script_pubkey())
}

/// Derive the blinding secret key from a confidential descriptor and a script pubkey.
pub fn derive_blinding_key(
    descriptor: &ConfidentialDescriptor<DescriptorPublicKey>,
    script_pubkey: &Script,
) -> Option<SecretKey> {
    let secp = Secp256k1::new();
    match &descriptor.key {
        Key::Slip77(k) => Some(k.blinding_private_key(script_pubkey)),
        Key::View(DescriptorSecretKey::XPrv(dxk)) => {
            let k = dxk.xkey.to_priv();
            Some(tweak_private_key(&secp, script_pubkey, &k.inner))
        }
        Key::View(DescriptorSecretKey::Single(k)) => {
            Some(tweak_private_key(&secp, script_pubkey, &k.key.inner))
        }
        _ => None,
    }
}

fn commitments(
    secp: &Secp256k1<All>,
    txout_secrets: &TxOutSecrets,
) -> (Generator, PedersenCommitment) {
    let asset_comm = Generator::new_blinded(
        secp,
        txout_secrets.asset.into_inner().to_byte_array().into(),
        txout_secrets.asset_bf.into_inner(),
    );
    let amount_comm = PedersenCommitment::new(
        secp,
        txout_secrets.value,
        txout_secrets.value_bf.into_inner(),
        asset_comm,
    );
    (asset_comm, amount_comm)
}

fn is_mine(
    script_pubkey: &Script,
    descriptor: &ConfidentialDescriptor<DescriptorPublicKey>,
    bip32_derivation: &BTreeMap<PublicKey, KeySource>,
) -> Result<bool, Error> {
    for (_, path) in bip32_derivation.values() {
        // TODO should I check descriptor derivation path is compatible with given bip32_derivation?
        // TODO consider fingerprint if available
        if path.is_empty() {
            continue;
        }
        let wildcard_index = path[path.len() - 1];
        for d in descriptor.descriptor.clone().into_single_descriptors()? {
            // TODO improve by checking only the descriptor ending with the given path
            let mine = d
                .at_derivation_index(wildcard_index.into())?
                .script_pubkey();
            if &mine == script_pubkey {
                return Ok(true);
            }
        }
    }
    Ok(false)
}

/// Return the net balance of a PSET from the perspective of the given `descriptor`.
/// It returns also the fee and the recipients (external receivers) of the PSET.
pub fn pset_balance(
    pset: &PartiallySignedTransaction,
    descriptor: &ConfidentialDescriptor<DescriptorPublicKey>,
    params: &'static elements::AddressParams,
) -> Result<PsetBalance, Error> {
    let secp = Secp256k1::new();
    let mut balances: BTreeMap<AssetId, i64> = BTreeMap::new();
    let mut fee: Option<u64> = None;
    for (idx, input) in pset.inputs().iter().enumerate() {
        match input.witness_utxo.as_ref() {
            None => {
                let previous_outpoint = OutPoint {
                    txid: input.previous_txid,
                    vout: input.previous_output_index,
                };
                return Err(Error::MissingPreviousOutput {
                    idx,
                    previous_outpoint,
                });
            }
            Some(txout) => {
                if !is_mine(&txout.script_pubkey, descriptor, &input.bip32_derivation)
                    .unwrap_or(false)
                {
                    // Ignore outputs we don't own
                    continue;
                }

                if input.is_pegin() {
                    return Err(Error::InputPeginUnsupported { idx });
                }
                if input.has_issuance() {
                    let issuance = input.asset_issuance();
                    if issuance.amount.is_confidential()
                        || issuance.inflation_keys.is_confidential()
                    {
                        return Err(Error::InputBlindedIssuance { idx });
                    }
                }

                // We expect the input to be blinded
                let (asset_comm, amount_comm) = match (txout.asset, txout.value) {
                    (Asset::Confidential(g), Value::Confidential(c)) => (g, c),
                    _ => return Err(Error::InputNotBlinded { idx }),
                };

                let (asset, value) = match (
                    input.blind_asset_proof.as_ref(),
                    input.blind_value_proof.as_ref(),
                    input.asset,
                    input.amount,
                ) {
                    (Some(bap), Some(bvp), Some(asset), Some(value)) => {
                        if !bap.blind_asset_proof_verify(&secp, asset, asset_comm) {
                            return Err(Error::InvalidAssetBlindProof { idx });
                        }
                        if !bvp.blind_value_proof_verify(&secp, value, asset_comm, amount_comm) {
                            return Err(Error::InvalidValueBlindProof { idx });
                        }
                        (asset, value)
                    }
                    _ => {
                        // To handle PSETs created before we started adding input blind proofs,
                        // we also try to unblind the input with the descriptor blinding key
                        let private_blinding_key =
                            derive_blinding_key(descriptor, &txout.script_pubkey)
                                .ok_or(Error::MissingPrivateBlindingKey)?;
                        // However the rangeproof is stored in another field
                        // since the output witness, which includes the rangeproof,
                        // is not serialized.
                        let mut txout_with_rangeproof = txout.clone();
                        txout_with_rangeproof
                            .witness
                            .rangeproof
                            .clone_from(&input.in_utxo_rangeproof);
                        let txout_secrets = txout_with_rangeproof
                            .unblind(&secp, private_blinding_key)
                            .map_err(|_| Error::InputMineNotUnblindable { idx })?;
                        if (asset_comm, amount_comm) != commitments(&secp, &txout_secrets) {
                            return Err(Error::InputCommitmentsMismatch { idx });
                        }
                        (txout_secrets.asset, txout_secrets.value)
                    }
                };

                *balances.entry(asset).or_default() -= value as i64;
            }
        }
    }

    let mut recipients = vec![];
    for (idx, output) in pset.outputs().iter().enumerate() {
        if output.script_pubkey.is_empty() {
            // Candidate fee output
            if fee.is_some() {
                return Err(Error::MultipleFee);
            }
            if output.asset.is_none()
                || output.asset_comm.is_some()
                || output.amount.is_none()
                || output.amount_comm.is_some()
            {
                return Err(Error::BlindedFee);
            }
            fee = Some(output.amount.expect("previous if prevent this to be none"));
            continue;
        }

        if !is_mine(&output.script_pubkey, descriptor, &output.bip32_derivation).unwrap_or(false) {
            // external recipients
            let blinding_pubkey = output.blinding_key.as_ref().map(|k| k.inner);
            let address =
                elements::Address::from_script(&output.script_pubkey, blinding_pubkey, params);
            let recipient = Recipient {
                address,
                vout: idx as u32,
                asset: output.asset,
                value: output.amount,
            };

            recipients.push(recipient);

            continue;
        }

        // Expect all outputs to be blinded and with blind proofs
        match (
            output.asset,
            output.asset_comm,
            output.blind_asset_proof.as_ref(),
            output.amount,
            output.amount_comm,
            output.blind_value_proof.as_ref(),
        ) {
            (None, _, _, None, _, _) => return Err(Error::OutputAssetValueNone { idx }),
            (None, _, _, Some(_), _, _) => return Err(Error::OutputValueNone { idx }),
            (Some(_), _, _, None, _, _) => return Err(Error::OutputAssetNone { idx }),
            (
                Some(asset),
                Some(asset_comm),
                Some(blind_asset_proof),
                Some(amount),
                Some(amount_comm),
                Some(blind_value_proof),
            ) => {
                if !blind_asset_proof.blind_asset_proof_verify(&secp, asset, asset_comm) {
                    return Err(Error::InvalidAssetBlindProof { idx });
                }
                if !blind_value_proof.blind_value_proof_verify(
                    &secp,
                    amount,
                    asset_comm,
                    amount_comm,
                ) {
                    return Err(Error::InvalidValueBlindProof { idx });
                }

                // Check that we can later unblind the output
                let private_blinding_key = derive_blinding_key(descriptor, &output.script_pubkey)
                    .ok_or(Error::MissingPrivateBlindingKey)?;
                let txout_secrets = output
                    .to_txout()
                    .unblind(&secp, private_blinding_key)
                    .map_err(|_| Error::OutputMineNotUnblindable { idx })?;
                if (asset_comm, amount_comm) != commitments(&secp, &txout_secrets) {
                    return Err(Error::OutputCommitmentsMismatch { idx });
                }

                *balances.entry(asset).or_default() += amount as i64;
            }
            _ => return Err(Error::OutputNotBlinded { idx }),
        }
    }
    let fee = fee.unwrap_or(0);

    // Remove assets with 0 balance which are not changing the net balance.
    // For example it happens with reissuance tokens.
    balances.retain(|_, v| *v != 0);

    Ok(PsetBalance {
        fee,
        balances: balances.into(),
        recipients,
    })
}

/// Return the signatures of a PSET, for each input return a [`PsetSignatures`] which includes a
/// list of signatures that are available and a list of signatures that are missing.
pub fn pset_signatures(pset: &PartiallySignedTransaction) -> Vec<PsetSignatures> {
    pset.inputs()
        .iter()
        .map(|input| {
            let mut has_signature = vec![];
            let mut missing_signature = vec![];
            for (pk, ks) in input.bip32_derivation.clone() {
                if input.partial_sigs.contains_key(&pk) {
                    has_signature.push((pk, ks));
                } else {
                    missing_signature.push((pk, ks));
                }
            }
            PsetSignatures {
                has_signature,
                missing_signature,
            }
        })
        .collect()
}

/// Return the issuances of a PSET, for each input return an Issuance but the struct must be checked with [`Issuance::is_issuance`] if it's a real issuance or reissuance.
pub fn pset_issuances(pset: &PartiallySignedTransaction) -> Vec<Issuance> {
    pset.inputs().iter().map(Issuance::new).collect()
}

/// Create the same burn script that Elements Core wallet creates
pub fn burn_script() -> Script {
    Builder::new().push_opcode(OP_RETURN).into_script()
}

/// Create a debug string of a PSET, but remove new lines on number arrays.
pub fn pset_debug(pset: &PartiallySignedTransaction) -> String {
    let debug_str = format!("{pset:#?}");
    let mut result = String::new();

    // Remove new line for lines that contain only a number ending with a comma so that the output is more readable
    for line in debug_str.lines() {
        let trimmed = line.trim();

        if trimmed.ends_with(',') {
            if let Some(num_part) = trimmed.strip_suffix(",") {
                if num_part.parse::<u64>().is_ok() {
                    result.push_str(trimmed);
                    continue;
                }
            }
        }

        // For other lines, append with newline
        result.push_str(line);
        result.push('\n');
    }

    result
}

#[cfg(test)]
mod test {
    use elements::{pset::PartiallySignedTransaction, AssetId};
    use elements_miniscript::{ConfidentialDescriptor, DescriptorPublicKey};

    use crate::pset_balance;

    fn normalize_newlines(s: &str) -> String {
        s.replace("\r\n", "\n")
    }

    fn setup_pset_details() -> (AssetId, ConfidentialDescriptor<DescriptorPublicKey>) {
        let asset_id_str = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5";
        let asset_id: AssetId = asset_id_str.parse().unwrap();
        let desc_str = include_str!("../test_data/pset_details/descriptor");
        let desc: ConfidentialDescriptor<DescriptorPublicKey> = desc_str.parse().unwrap();
        (asset_id, desc)
    }

    #[test]
    fn test_pset_details_redeposit_balance() {
        let (asset_id, desc) = setup_pset_details();
        let pset_str = include_str!("../test_data/pset_details/pset.base64");
        let pset: PartiallySignedTransaction = pset_str.parse().unwrap();
        let balance = pset_balance(&pset, &desc, &elements::AddressParams::LIQUID_TESTNET).unwrap();
        assert!(
            !balance.balances.contains_key(&asset_id),
            "redeposit (balance = 0) should disappear from the list"
        );

        // Same for newly created psets with blind proofs
        let pset_str =
            include_str!("../test_data/pset_details/pset_with_input_blind_proofs.base64");
        let pset: PartiallySignedTransaction = pset_str.parse().unwrap();
        let balance = pset_balance(&pset, &desc, &elements::AddressParams::LIQUID_TESTNET).unwrap();
        assert!(
            !balance.balances.contains_key(&asset_id),
            "redeposit (balance = 0) should disappear from the list"
        );
    }

    #[test]
    fn test_pset_details_negative_balance() {
        let (asset_id, desc) = setup_pset_details();
        let pset_str = include_str!("../test_data/pset_details/pset2.base64");
        let pset: PartiallySignedTransaction = pset_str.parse().unwrap();
        let balance = pset_balance(&pset, &desc, &elements::AddressParams::LIQUID_TESTNET).unwrap();
        let v = balance.balances.get(&asset_id).unwrap();
        assert_eq!(*v, -1);

        // Same for newly created psets with blind proofs
        let pset_str =
            include_str!("../test_data/pset_details/pset2_with_input_blind_proofs.base64");
        let pset: PartiallySignedTransaction = pset_str.parse().unwrap();
        let balance = pset_balance(&pset, &desc, &elements::AddressParams::LIQUID_TESTNET).unwrap();
        let v = balance.balances.get(&asset_id).unwrap();
        assert_eq!(*v, -1);
    }

    #[test]
    fn test_pset_outputs() {
        let pset_str = include_str!("../test_data/pset_outputs/pset.base64");
        let pset: PartiallySignedTransaction = pset_str.parse().unwrap();
        let desc_str = include_str!("../test_data/pset_outputs/descriptor");
        let desc = desc_str.parse().unwrap();
        let expected_dest = "tlq1qqwx9sng3htz6u2yeqrgf2w525att79vnvwtcqsar7xyqj8hf7s32usgvct9q9f4u3nmnnkwhkfayswc853egs7cvnfs3t7zty";
        let expected_asset_id = "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49";
        let expected_value = 120;
        let balance = pset_balance(&pset, &desc, &elements::AddressParams::LIQUID_TESTNET).unwrap();
        assert_eq!(balance.recipients.len(), 1);
        let recipient = balance.recipients.first().unwrap();
        let dest = recipient.address.as_ref().unwrap();
        assert_eq!(dest.to_string(), expected_dest);
        assert_eq!(recipient.asset.unwrap().to_string(), expected_asset_id);
        assert_eq!(recipient.value.unwrap(), expected_value);
        assert_eq!(recipient.vout, 0);

        let balance = pset_balance(&pset, &desc, &elements::AddressParams::LIQUID).unwrap();
        assert_eq!(balance.recipients.len(), 1);
        let recipient = balance.recipients.first().unwrap();
        let dest = recipient.address.as_ref().unwrap();
        assert_ne!(dest.to_string(), expected_dest);
        assert_eq!(dest.to_string(), "lq1qqwx9sng3htz6u2yeqrgf2w525att79vnvwtcqsar7xyqj8hf7s32usgvct9q9f4u3nmnnkwhkfayswc853egsw4pnw8lktr6d");
        assert_eq!(recipient.asset.unwrap().to_string(), expected_asset_id);
        assert_eq!(recipient.value.unwrap(), expected_value);
        assert_eq!(recipient.vout, 0);
    }

    #[test]
    fn test_pset_debug() {
        let pset_str = include_str!("../test_data/pset_outputs/pset.base64");
        let pset: PartiallySignedTransaction = pset_str.parse().unwrap();
        let debug = crate::pset_debug(&pset);
        let expected = include_str!("../test_data/pset_debug.txt");
        assert_eq!(normalize_newlines(&debug), normalize_newlines(expected));
    }
}