• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tari-project / tari / 18039653182

26 Sep 2025 01:46PM UTC coverage: 60.88% (-0.08%) from 60.957%
18039653182

push

github

web-flow
fix: update ledger wallet sdk to newer ledger firmware (#7520)

Description
---
- Updated ledger wallet to use the latest ledger_device_sdk.
- Also updated the ledger wallet rust toolchain.
- Fixed double `comm.reply_ok();` sent from the ledger device - this
caused the the accessor methods to receive an empty data array, the
first containing the data and the second containing the empty array.

Motivation and Context
---
See above.

How Has This Been Tested?
---
- System-level testing passed (send & recieve).
- Demo passed:
```
cargo run --release --example ledger_demo
    Finished `release` profile [optimized] target(s) in 1.11s
     Running `target\release\examples\ledger_demo.exe`

Transport created in 26.7992ms
Transport created in 11.8355ms
Transport created in 14.8429ms
Transport created in 11.8416ms
Transport created in 12.8842ms
Transport created in 11.903ms
Transport created in 13.8822ms
Transport created in 11.9641ms
Transport created in 14.883ms
Transport created in 11.8267ms

Result data: [1, 98, 229, 50, 140, 22, 238, 33, 222, 42, 25, 252, 173, 215, 208, 105, 100, 217, 79, 187, 33, 141, 151, 212, 144, 248, 237, 197, 107, 68, 34, 39, 121]
Application verified in 1.6788405s
Application verified in 200ns
Application verified in 100ns
Application verified in 100ns
Application verified in 100ns
Application verified in 100ns
Application verified in 100ns
Application verified in 100ns
Application verified in 100ns
Application verified in 100ns


test: GetAppName
app name:       minotari_ledger_wallet

test: GetVersion
version:        5.1.0-pre.2

test: GetPublicAlpha
public_alpha:   d4edcf00d86e4d62823e900a906bab46a

test: GetPublicKey
Result data: [1, 90, 154, 86, 152, 203, 121, 156, 164, 81, 186, 107, 138, 195, 132, 157, 244, 235, 38, 183, 72, 223, 220, 1, 29, 94, 184, 211, 220, 75, 84, 53, 59]
public_key:     5a9a5698cdfdc011d5eb8d3dc4b54353b
Result data: [1, 88, 73, 90, 146, 76, 10... (continued)

0 of 5 new or added lines in 1 file covered. (0.0%)

2180 existing lines in 29 files now uncovered.

74589 of 122519 relevant lines covered (60.88%)

222551.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.25
/base_layer/transaction_components/src/key_manager/interface.rs
1
// Copyright 2023 The Tari Project
2
//
3
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
4
// following conditions are met:
5
//
6
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
7
// disclaimer.
8
//
9
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
10
// following disclaimer in the documentation and/or other materials provided with the distribution.
11
//
12
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
13
// products derived from this software without specific prior written permission.
14
//
15
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
16
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
18
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
20
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
21
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22

23
use std::{fmt, str::FromStr};
24

25
use blake2::Blake2b;
26
use digest::consts::U64;
27
use serde::{Deserialize, Serialize};
28
use strum_macros::EnumIter;
29
use tari_common_types::{
30
    tari_address::TariAddress,
31
    types::{
32
        ComAndPubSignature,
33
        CommsDHKE,
34
        CompressedCommitment,
35
        CompressedPublicKey,
36
        CompressedSignature,
37
        PrivateKey,
38
        RangeProof,
39
        WalletMessageSchnorrSignature,
40
    },
41
    WALLET_COMMS_AND_SPEND_KEY_BRANCH,
42
};
43
use tari_crypto::{hashing::DomainSeparatedHash, ristretto::RistrettoComSig};
44
use tari_script::{CompressedCheckSigSchnorrSignature, TariScript};
45
use tari_utilities::hex::{from_hex, Hex};
46

47
use crate::key_manager::AddResult;
48

49
pub const MANAGED_KEY_BRANCH: &str = "managed";
50
pub const DERIVED_KEY_BRANCH: &str = "derived";
51
pub const IMPORTED_KEY_BRANCH: &str = "imported";
52
pub const ZERO_KEY_BRANCH: &str = "zero";
53
pub const DH_COMMITMENT_MASK_BRANCH: &str = "dh_commitment_mask";
54
pub const DH_ENCRYPTED_DATA_BRANCH: &str = "dh_encrypted_data";
55
pub const ENCRYPTED_BRANCH: &str = "encrypted";
56

57
use crate::{
58
    key_manager::error::{KeyManagerServiceError, KeyManagerStorageError},
59
    transaction_components::{
60
        EncryptedData,
61
        KernelFeatures,
62
        MemoField,
63
        RangeProofType,
64
        TransactionError,
65
        TransactionInputVersion,
66
        TransactionKernelVersion,
67
        TransactionOutputVersion,
68
    },
69
    MicroMinotari,
70
};
71

72
#[repr(u8)]
UNCOV
73
#[derive(Clone, Copy, EnumIter)]
×
74
pub enum KeyManagerBranch {
75
    Comms,
76
}
77

78
impl KeyManagerBranch {
79
    /// Warning: Changing these strings will affect the backwards compatibility of the wallet with older databases or
80
    /// recovery.
81
    pub fn get_branch_key(self) -> String {
×
82
        match self {
×
UNCOV
83
            KeyManagerBranch::Comms => WALLET_COMMS_AND_SPEND_KEY_BRANCH.to_string(),
×
UNCOV
84
        }
×
85
    }
×
86
}
87

88
/// TariKeyId Variants and Private Key Calculation
89
// 1. Managed { branch, index }
90
// Description: Represents a key derived from a deterministic key manager using a specific branch and index.
91
// Private Key Calculation:
92
// The private key is deterministically derived using the key manager's master seed, the branch string, and the index.
93
// Formula: private_key = derive(master_seed, branch, index)
94
// The derivation uses a cryptographic key derivation function (KDF) such as HKDF or similar, ensuring that the same
95
// inputs always produce the same private key.
96
// 2. Derived { key }
97
// Description: Represents a key derived from a serialized key string.
98
// Private Key Calculation:
99
// The serialized key string encodes the derivation path or method. The key manager parses this string and applies the
100
// appropriate derivation logic to reconstruct the private key.
101
// 3. Imported { key }
102
// Description: Represents a key that was imported directly.
103
// Private Key Calculation:
104
// The private key is stored in the key manager's backend, associated with the given public key.
105
// Retrieval: The key manager looks up the private key using the public key.
106
// 4. Zero
107
// Description: Represents a special zero key.
108
// Private Key Calculation:
109
// The private key is a constant value, typically all zeros, and is not used for real cryptographic operations.
110
// 5. DHCommitmentMask { public_key, private_key } and DHEncryptedData { public_key, private_key }
111
// Description: Used for Diffie-Hellman operations, storing both a public and a serialized private key.
112
// Private Key Calculation:
113
// The private key is reconstructed from the serialized string, which may represent a derived or imported key.
114
// 6. Encrypted { encrypted, key }
115
// Description: Represents a key that is encrypted.
116
// Private Key Calculation:
117
// The encrypted bytes are decrypted using the provided key string, which is used as a decryption key or derivation
118
// path.
UNCOV
119
#[derive(Default, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
×
120
pub enum TariKeyId {
121
    Managed {
122
        branch: String,
123
        index: u64,
124
    },
125
    Derived {
126
        key: SerializedKeyString,
127
    },
128
    Imported {
129
        key: CompressedPublicKey,
130
    },
131
    #[default]
132
    Zero,
133
    DHCommitmentMask {
134
        public_key: CompressedPublicKey,
135
        private_key: SerializedKeyString,
136
    },
137
    DHEncryptedData {
138
        public_key: CompressedPublicKey,
139
        private_key: SerializedKeyString,
140
    },
141
    Encrypted {
142
        encrypted: Vec<u8>,
143
        key: SerializedKeyString,
144
    },
145
}
146

147
impl TariKeyId {
UNCOV
148
    pub fn managed_index(&self) -> Option<u64> {
×
UNCOV
149
        match self {
×
UNCOV
150
            TariKeyId::Managed { index, .. } => Some(*index),
×
UNCOV
151
            TariKeyId::Derived { .. } => None,
×
UNCOV
152
            TariKeyId::Imported { .. } => None,
×
UNCOV
153
            TariKeyId::Zero => None,
×
154
            TariKeyId::DHCommitmentMask { .. } => None,
×
UNCOV
155
            TariKeyId::DHEncryptedData { .. } => None,
×
UNCOV
156
            TariKeyId::Encrypted { .. } => None,
×
157
        }
UNCOV
158
    }
×
159

UNCOV
160
    pub fn managed_branch(&self) -> Option<String> {
×
UNCOV
161
        match self {
×
UNCOV
162
            TariKeyId::Managed { branch, .. } => Some(branch.clone()),
×
UNCOV
163
            TariKeyId::Derived { .. } => None,
×
164
            TariKeyId::Imported { .. } => None,
×
UNCOV
165
            TariKeyId::Zero => None,
×
UNCOV
166
            TariKeyId::DHCommitmentMask { .. } => None,
×
UNCOV
167
            TariKeyId::DHEncryptedData { .. } => None,
×
UNCOV
168
            TariKeyId::Encrypted { .. } => None,
×
169
        }
UNCOV
170
    }
×
171

172
    pub fn imported(&self) -> Option<CompressedPublicKey> {
×
UNCOV
173
        match self {
×
UNCOV
174
            TariKeyId::Managed { .. } => None,
×
UNCOV
175
            TariKeyId::Derived { .. } => None,
×
UNCOV
176
            TariKeyId::Imported { key } => Some(key.clone()),
×
UNCOV
177
            TariKeyId::Zero => None,
×
UNCOV
178
            TariKeyId::DHCommitmentMask { .. } => None,
×
UNCOV
179
            TariKeyId::DHEncryptedData { .. } => None,
×
UNCOV
180
            TariKeyId::Encrypted { .. } => None,
×
181
        }
UNCOV
182
    }
×
183
}
184

185
impl FromStr for TariKeyId {
186
    type Err = String;
187

188
    fn from_str(id: &str) -> Result<Self, Self::Err> {
6,231✔
189
        let parts: Vec<&str> = id.split('.').collect();
6,231✔
190
        match parts.first() {
6,231✔
UNCOV
191
            None => Err("Out of bounds".to_string()),
×
192
            Some(val) => match *val {
6,231✔
193
                MANAGED_KEY_BRANCH => {
6,231✔
194
                    if parts.len() != 3 {
6,199✔
UNCOV
195
                        return Err("Wrong managed format".to_string());
×
196
                    }
6,199✔
197
                    let index = parts
6,199✔
198
                        .get(2)
6,199✔
199
                        .expect("Already checked")
6,199✔
200
                        .parse()
6,199✔
201
                        .map_err(|_| "Index for default, invalid u64".to_string())?;
6,199✔
202
                    Ok(TariKeyId::Managed {
6,199✔
203
                        branch: (*parts.get(1).expect("Already checked")).into(),
6,199✔
204
                        index,
6,199✔
205
                    })
6,199✔
206
                },
207
                IMPORTED_KEY_BRANCH => {
32✔
208
                    if parts.len() != 2 {
1✔
209
                        return Err("Wrong imported format".to_string());
×
210
                    }
1✔
211
                    let key = CompressedPublicKey::from_hex(parts.get(1).expect("Already checked"))
1✔
212
                        .map_err(|_| "Invalid public key".to_string())?;
1✔
213
                    Ok(TariKeyId::Imported { key })
1✔
214
                },
215
                ZERO_KEY_BRANCH => Ok(TariKeyId::Zero),
31✔
216
                DERIVED_KEY_BRANCH => {
30✔
217
                    if parts.len() < 3 {
2✔
UNCOV
218
                        return Err("Wrong derived format".to_string());
×
219
                    };
2✔
220

2✔
221
                    let key = parts.get(1..).expect("Already checked").join(".");
2✔
222
                    Ok(TariKeyId::Derived {
2✔
223
                        key: SerializedKeyString::from(key),
2✔
224
                    })
2✔
225
                },
226
                DH_COMMITMENT_MASK_BRANCH => {
28✔
227
                    if parts.len() < 3 {
5✔
UNCOV
228
                        return Err("Wrong dh_commitment_mask format".to_string());
×
229
                    }
5✔
230
                    let public_key = CompressedPublicKey::from_hex(parts.get(1).expect("Already checked"))
5✔
231
                        .map_err(|_| "Invalid public key".to_string())?;
5✔
232
                    let private_key = parts.get(2..).expect("Already checked").join(".");
5✔
233
                    Ok(TariKeyId::DHCommitmentMask {
5✔
234
                        public_key,
5✔
235
                        private_key: SerializedKeyString::from(private_key),
5✔
236
                    })
5✔
237
                },
238
                DH_ENCRYPTED_DATA_BRANCH => {
23✔
239
                    if parts.len() < 3 {
1✔
UNCOV
240
                        return Err("Wrong encryted data format".to_string());
×
241
                    }
1✔
242
                    let public_key = CompressedPublicKey::from_hex(parts.get(1).expect("Already checked"))
1✔
243
                        .map_err(|_| "Invalid public key".to_string())?;
1✔
244
                    let private_key = parts.get(2..).expect("Already checked").join(".");
1✔
245
                    Ok(TariKeyId::DHEncryptedData {
1✔
246
                        public_key,
1✔
247
                        private_key: SerializedKeyString::from(private_key),
1✔
248
                    })
1✔
249
                },
250
                ENCRYPTED_BRANCH => {
22✔
251
                    if parts.len() < 3 {
22✔
UNCOV
252
                        return Err("Wrong encrypted format".to_string());
×
253
                    }
22✔
254
                    let encrypted: Vec<u8> = from_hex(parts.get(1).expect("Already checked"))
22✔
255
                        .map_err(|_| "Invalid encrypted bytes".to_string())?;
22✔
256
                    let key = parts.get(2..).expect("Already checked").join(".");
22✔
257
                    Ok(TariKeyId::Encrypted {
22✔
258
                        encrypted,
22✔
259
                        key: SerializedKeyString::from(key),
22✔
260
                    })
22✔
261
                },
UNCOV
262
                _ => Err("Wrong generic format".to_string()),
×
263
            },
264
        }
265
    }
6,231✔
266
}
267

268
impl fmt::Display for TariKeyId {
269
    // This trait requires `fmt` with this exact signature.
270
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2,328✔
271
        match self {
2,328✔
272
            TariKeyId::Managed { branch, index } => write!(f, "{MANAGED_KEY_BRANCH}.{branch}.{index}"),
2,299✔
273
            TariKeyId::Derived { key } => write!(f, "{DERIVED_KEY_BRANCH}.{key}"),
8✔
274
            TariKeyId::Imported { key: public_key } => write!(f, "{IMPORTED_KEY_BRANCH}.{public_key}"),
1✔
275
            TariKeyId::Zero => write!(f, "{ZERO_KEY_BRANCH}"),
1✔
276
            TariKeyId::DHCommitmentMask {
277
                public_key,
4✔
278
                private_key,
4✔
279
            } => {
4✔
280
                write!(f, "{DH_COMMITMENT_MASK_BRANCH}.{public_key}.{private_key}")
4✔
281
            },
282
            TariKeyId::DHEncryptedData {
283
                public_key,
1✔
284
                private_key,
1✔
285
            } => {
1✔
286
                write!(f, "{DH_ENCRYPTED_DATA_BRANCH}.{public_key}.{private_key}")
1✔
287
            },
288
            TariKeyId::Encrypted { encrypted, key } => {
14✔
289
                write!(f, "{ENCRYPTED_BRANCH}.{}.{}", encrypted.to_hex(), key)
14✔
290
            },
291
        }
292
    }
2,328✔
293
}
294

295
#[derive(Debug, Eq, PartialEq)]
296
pub struct TariKeyAndId {
297
    pub pub_key: CompressedPublicKey,
298
    pub key_id: TariKeyId,
299
}
300

UNCOV
301
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
×
302
pub struct SerializedKeyString {
303
    inner: String,
304
}
305

306
impl From<String> for SerializedKeyString {
307
    fn from(inner: String) -> Self {
2,340✔
308
        Self { inner }
2,340✔
309
    }
2,340✔
310
}
311

312
impl From<&str> for SerializedKeyString {
UNCOV
313
    fn from(inner: &str) -> Self {
×
UNCOV
314
        Self { inner: inner.into() }
×
UNCOV
315
    }
×
316
}
317

318
impl fmt::Display for SerializedKeyString {
319
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
6,251✔
320
        write!(f, "{}", self.inner)
6,251✔
321
    }
6,251✔
322
}
323

324
impl From<TariKeyId> for SerializedKeyString {
325
    fn from(key_id: TariKeyId) -> Self {
915✔
326
        Self::from(key_id.to_string())
915✔
327
    }
915✔
328
}
329

330
impl From<&TariKeyId> for SerializedKeyString {
331
    fn from(key_id: &TariKeyId) -> Self {
1,394✔
332
        Self::from(key_id.to_string())
1,394✔
333
    }
1,394✔
334
}
335

336
#[derive(Clone, Copy, PartialEq)]
337
pub enum TxoStage {
338
    Input,
339
    Output,
340
}
341

342
#[async_trait::async_trait]
343
pub trait TransactionKeyManagerInterface: Clone + Send + Sync + 'static {
344
    /// Creates a new branch for the key manager service to track
345
    /// If this is an existing branch, that is not yet tracked in memory, the key manager service will load the key
346
    /// manager from the backend to track in memory, will return `Ok(AddResult::NewEntry)`. If the branch is already
347
    /// tracked in memory the result will be `Ok(AddResult::AlreadyExists)`. If the branch does not exist in memory
348
    /// or in the backend, a new branch will be created and tracked the backend, `Ok(AddResult::NewEntry)`.
349
    async fn add_new_branch<T: Into<String> + Send>(&self, branch: T) -> Result<AddResult, KeyManagerServiceError>;
350

351
    /// Gets the next key id from the branch. This will auto-increment the branch key index by 1
352
    async fn get_next_key<T: Into<String> + Send>(&self, branch: T) -> Result<TariKeyAndId, KeyManagerServiceError>;
353

354
    /// Gets a randomly generated key, which the key manager will manage
355
    async fn get_random_key(&self) -> Result<TariKeyAndId, KeyManagerServiceError>;
356

357
    /// Gets the fixed key id from the branch. This will use the branch key with index 0
358
    async fn get_static_key<T: Into<String> + Send>(&self, branch: T) -> Result<TariKeyId, KeyManagerServiceError>;
359

360
    /// Gets the key id at the specified index
361
    async fn get_public_key_at_key_id(&self, key_id: &TariKeyId)
362
        -> Result<CompressedPublicKey, KeyManagerServiceError>;
363

364
    /// Add a new key to be tracked
365
    async fn import_key(
366
        &self,
367
        private_key: PrivateKey,
368
        encryption_key: Option<TariKeyId>,
369
    ) -> Result<TariKeyId, KeyManagerServiceError>;
370

371
    async fn create_encrypted_key_from_existing_key(
372
        &self,
373
        key_id: &TariKeyId,
374
        encryption_key: Option<TariKeyId>,
375
    ) -> Result<TariKeyId, KeyManagerServiceError>;
376

377
    /// Gets the pedersen commitment for the specified index
378
    async fn get_commitment(
379
        &self,
380
        commitment_mask_key_id: &TariKeyId,
381
        value: &PrivateKey,
382
    ) -> Result<CompressedCommitment, KeyManagerServiceError>;
383

384
    async fn verify_mask(
385
        &self,
386
        commitment: &CompressedCommitment,
387
        commitment_mask_key_id: &TariKeyId,
388
        value: u64,
389
    ) -> Result<bool, KeyManagerServiceError>;
390

391
    async fn get_view_key(&self) -> Result<TariKeyAndId, KeyManagerServiceError>;
392

393
    async fn get_private_view_key(&self) -> Result<PrivateKey, KeyManagerServiceError>;
394

395
    async fn get_spend_key(&self) -> Result<TariKeyAndId, KeyManagerServiceError>;
396

397
    async fn get_comms_key(&self) -> Result<TariKeyAndId, KeyManagerServiceError>;
398

399
    async fn get_next_commitment_mask_and_script_key(
400
        &self,
401
    ) -> Result<(TariKeyAndId, TariKeyAndId), KeyManagerServiceError>;
402

403
    async fn find_script_key_id_from_commitment_mask_key_id(
404
        &self,
405
        commitment_mask_key_id: &TariKeyId,
406
        public_script_key: Option<&CompressedPublicKey>,
407
    ) -> Result<Option<TariKeyId>, KeyManagerServiceError>;
408

409
    async fn get_diffie_hellman_shared_secret(
410
        &self,
411
        secret_key_id: &TariKeyId,
412
        public_key: &CompressedPublicKey,
413
    ) -> Result<CommsDHKE, KeyManagerServiceError>;
414

415
    async fn get_diffie_hellman_stealth_domain_hasher(
416
        &self,
417
        secret_key_id: &TariKeyId,
418
        public_key: &CompressedPublicKey,
419
    ) -> Result<DomainSeparatedHash<Blake2b<U64>>, TransactionError>;
420

421
    async fn construct_range_proof(
422
        &self,
423
        commitment_mask_key_id: &TariKeyId,
424
        value: u64,
425
        min_value: u64,
426
    ) -> Result<RangeProof, TransactionError>;
427

428
    async fn get_script_signature(
429
        &self,
430
        script_key_id: &TariKeyId,
431
        commitment_mask_key_id: &TariKeyId,
432
        value: &PrivateKey,
433
        txi_version: &TransactionInputVersion,
434
        script_message: &[u8; 32],
435
    ) -> Result<ComAndPubSignature, TransactionError>;
436

437
    async fn get_partial_script_signature(
438
        &self,
439
        commitment_mask_id: &TariKeyId,
440
        value: &PrivateKey,
441
        txi_version: &TransactionInputVersion,
442
        ephemeral_pubkey: &CompressedPublicKey,
443
        script_public_key: &CompressedPublicKey,
444
        script_message: &[u8; 32],
445
    ) -> Result<ComAndPubSignature, TransactionError>;
446

447
    async fn get_partial_txo_kernel_signature(
448
        &self,
449
        commitment_mask_key_id: &TariKeyId,
450
        nonce_id: &TariKeyId,
451
        total_nonce: &CompressedPublicKey,
452
        total_excess: &CompressedPublicKey,
453
        kernel_version: &TransactionKernelVersion,
454
        kernel_message: &[u8; 32],
455
        kernel_features: &KernelFeatures,
456
        txo_type: TxoStage,
457
    ) -> Result<CompressedSignature, TransactionError>;
458

459
    async fn get_txo_kernel_signature_excess_with_offset(
460
        &self,
461
        commitment_mask_key_id: &TariKeyId,
462
        nonce: &TariKeyId,
463
    ) -> Result<CompressedPublicKey, TransactionError>;
464

465
    async fn get_txo_private_kernel_offset(
466
        &self,
467
        commitment_mask_key_id: &TariKeyId,
468
        nonce_id: &TariKeyId,
469
    ) -> Result<PrivateKey, TransactionError>;
470

471
    async fn encrypt_data_for_recovery(
472
        &self,
473
        commitment_mask_key_id: &TariKeyId,
474
        custom_recovery_key_id: Option<&TariKeyId>,
475
        value: u64,
476
        payment_id: MemoField,
477
    ) -> Result<EncryptedData, TransactionError>;
478

479
    async fn extract_payment_id_from_encrypted_data(
480
        &self,
481
        encrypted_data: &EncryptedData,
482
        commitment: &CompressedCommitment,
483
        custom_recovery_key_id: Option<&TariKeyId>,
484
    ) -> Result<MemoField, TransactionError>;
485

486
    async fn try_output_key_recovery(
487
        &self,
488
        commitment: &CompressedCommitment,
489
        encrypted_data: &EncryptedData,
490
        sender_offset_public_key: &CompressedPublicKey,
491
    ) -> Result<Option<(TariKeyId, MicroMinotari, MemoField)>, TransactionError>;
492

493
    async fn is_this_output_ours(
494
        &self,
495
        commitment: &CompressedCommitment,
496
        encrypted_data: &EncryptedData,
497
        custom_recovery_key_id: Option<PrivateKey>,
498
    ) -> Result<bool, TransactionError>;
499

500
    async fn get_script_offset(
501
        &self,
502
        script_key_ids: &[TariKeyId],
503
        sender_offset_key_ids: &[TariKeyId],
504
    ) -> Result<PrivateKey, TransactionError>;
505

506
    async fn get_metadata_signature_ephemeral_commitment(
507
        &self,
508
        nonce_id: &TariKeyId,
509
        range_proof_type: RangeProofType,
510
    ) -> Result<CompressedCommitment, TransactionError>;
511

512
    // Look into perhaps removing all nonce here, if the signer and receiver are the same it should not be required to
513
    // share or pre calc the nonces
514
    async fn get_metadata_signature(
515
        &self,
516
        commitment_mask_key_id: &TariKeyId,
517
        value_as_private_key: &PrivateKey,
518
        sender_offset_key_id: &TariKeyId,
519
        txo_version: &TransactionOutputVersion,
520
        metadata_signature_message: &[u8; 32],
521
        range_proof_type: RangeProofType,
522
    ) -> Result<ComAndPubSignature, TransactionError>;
523

524
    async fn get_one_sided_metadata_signature(
525
        &self,
526
        commitment_mask_key_id: &TariKeyId,
527
        value: MicroMinotari,
528
        sender_offset_key_id: &TariKeyId,
529
        txo_version: &TransactionOutputVersion,
530
        metadata_signature_message_common: &[u8; 32],
531
        range_proof_type: RangeProofType,
532
        script: &TariScript,
533
        receiver_address: &TariAddress,
534
    ) -> Result<ComAndPubSignature, TransactionError>;
535

536
    async fn sign_message_with_spend_key(
537
        &self,
538
        message: &[u8],
539
        sender_offset_key: Option<&CompressedPublicKey>,
540
    ) -> Result<WalletMessageSchnorrSignature, KeyManagerServiceError>;
541

542
    async fn sign_script_message(
543
        &self,
544
        private_key_id: &TariKeyId,
545
        challenge: &[u8],
546
    ) -> Result<CompressedCheckSigSchnorrSignature, TransactionError>;
547

548
    async fn sign_script_message_with_spend_key(
549
        &self,
550
        message: &[u8],
551
        sender_offset_pub_key: Option<&CompressedPublicKey>,
552
    ) -> Result<CompressedCheckSigSchnorrSignature, KeyManagerServiceError>;
553

554
    async fn sign_with_nonce_and_challenge(
555
        &self,
556
        private_key_id: &TariKeyId,
557
        nonce: &TariKeyId,
558
        challenge: &[u8; 64],
559
    ) -> Result<CompressedSignature, TransactionError>;
560

561
    async fn get_receiver_partial_metadata_signature(
562
        &self,
563
        commitment_mask_key_id: &TariKeyId,
564
        value: &PrivateKey,
565
        sender_offset_public_key: &CompressedPublicKey,
566
        ephemeral_pubkey: &CompressedPublicKey,
567
        txo_version: &TransactionOutputVersion,
568
        metadata_signature_message: &[u8; 32],
569
        range_proof_type: RangeProofType,
570
    ) -> Result<ComAndPubSignature, TransactionError>;
571

572
    // In the case where the sender is an aggregated signer, we need to parse in the other public key shares, this is
573
    // done in: aggregated_sender_offset_public_keys and aggregated_ephemeral_public_keys. If there is no aggregated
574
    // signers, this can be left as none
575
    async fn get_sender_partial_metadata_signature(
576
        &self,
577
        ephemeral_private_nonce_id: &TariKeyId,
578
        sender_offset_key_id: &TariKeyId,
579
        commitment: &CompressedCommitment,
580
        ephemeral_commitment: &CompressedCommitment,
581
        txo_version: &TransactionOutputVersion,
582
        metadata_signature_message: &[u8; 32],
583
    ) -> Result<ComAndPubSignature, TransactionError>;
584

585
    async fn generate_burn_proof(
586
        &self,
587
        commitment_mask_key_id: &TariKeyId,
588
        amount: &PrivateKey,
589
        claim_public_key: &CompressedPublicKey,
590
    ) -> Result<RistrettoComSig, TransactionError>;
591

592
    async fn stealth_address_script_spending_key(
593
        &self,
594
        commitment_mask_key_id: &TariKeyId,
595
        spend_key: &CompressedPublicKey,
596
    ) -> Result<CompressedPublicKey, TransactionError>;
597

598
    async fn add_offset_to_spend_key(
599
        &self,
600
        spend_key_id: &TariKeyId,
601
        sender_offset_pub_key: &CompressedPublicKey,
602
    ) -> Result<TariKeyId, KeyManagerServiceError>;
603

604
    async fn encrypted_key(
605
        &self,
606
        key_id: &TariKeyId,
607
        encryption_key_id: Option<&TariKeyId>,
608
    ) -> Result<Vec<u8>, KeyManagerServiceError>;
609

610
    async fn import_encrypted_key(
611
        &self,
612
        encrypted: Vec<u8>,
613
        encryption_key_id: Option<&TariKeyId>,
614
    ) -> Result<TariKeyId, KeyManagerServiceError>;
615
}
616

617
#[async_trait::async_trait]
618
pub trait SecretTransactionKeyManagerInterface: TransactionKeyManagerInterface {
619
    /// Gets the pedersen commitment for the specified index
620
    async fn get_private_key(&self, key_id: &TariKeyId) -> Result<PrivateKey, KeyManagerServiceError>;
621
}
622

623
/// This trait defines the required behaviour that a storage backend must provide for the Key Manager service.
624
#[async_trait::async_trait]
625
pub trait TransactionKeyManagerBackend: Clone + Send + Sync {
626
    /// This will retrieve the key manager specified by the branch string, None is returned if the key manager is not
627
    /// found for the branch.
628
    async fn get_key_manager(&self, branch: &str) -> Result<Option<KeyManagerState>, KeyManagerStorageError>;
629
    /// This will add an additional branch for the key manager to track.
630
    async fn add_key_manager(&self, key_manager: KeyManagerState) -> Result<(), KeyManagerStorageError>;
631
    /// This will increase the key index of the specified branch, and returns an error if the branch does not exist.
632
    async fn increment_key_index(&self, branch: &str) -> Result<(), KeyManagerStorageError>;
633
    /// This method will set the currently stored key index for the key manager.
634
    async fn set_key_index(&self, branch: &str, index: u64) -> Result<(), KeyManagerStorageError>;
635
    /// This method will import a new public private key pair into the database
636
    async fn insert_imported_key(
637
        &self,
638
        public_key: CompressedPublicKey,
639
        private_key: PrivateKey,
640
    ) -> Result<(), KeyManagerStorageError>;
641
    /// This method will retrieve  public private key pair from the database
642
    async fn get_imported_key(&self, public_key: &CompressedPublicKey) -> Result<PrivateKey, KeyManagerStorageError>;
643
}
644

645
/// Holds the state of the KeyManager for the branch
646
#[derive(Clone, Debug, PartialEq)]
647
pub struct KeyManagerState {
648
    pub branch_seed: String,
649
    pub primary_key_index: u64,
650
}
651

652
#[cfg(test)]
653
mod test {
654
    use core::iter;
655
    use std::str::FromStr;
656

657
    use rand::{distributions::Alphanumeric, rngs::OsRng, Rng};
658
    use tari_common_types::types::{CompressedPublicKey, PrivateKey};
659
    use tari_crypto::keys::SecretKey as SK;
660

661
    use crate::key_manager::TariKeyId;
662

663
    fn random_string(len: usize) -> String {
2✔
664
        iter::repeat(())
2✔
665
            .map(|_| OsRng.sample(Alphanumeric) as char)
13✔
666
            .take(len)
2✔
667
            .collect()
2✔
668
    }
2✔
669

670
    #[test]
671
    fn key_id_converts_correctly() {
1✔
672
        let managed_key_id: TariKeyId = TariKeyId::Managed {
1✔
673
            branch: random_string(8) + " " + &random_string(5),
1✔
674
            index: {
1✔
675
                let mut rng = rand::thread_rng();
1✔
676
                let random_value: u64 = rng.gen();
1✔
677
                random_value
1✔
678
            },
1✔
679
        };
1✔
680
        let imported_key_id = TariKeyId::Imported {
1✔
681
            key: CompressedPublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)),
1✔
682
        };
1✔
683
        let zero_key_id = TariKeyId::Zero;
1✔
684
        let derived_key_id = TariKeyId::Derived {
1✔
685
            key: managed_key_id.clone().into(),
1✔
686
        };
1✔
687

1✔
688
        let dh_commitment_mask_key_id = TariKeyId::DHCommitmentMask {
1✔
689
            public_key: CompressedPublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)),
1✔
690
            private_key: managed_key_id.clone().into(),
1✔
691
        };
1✔
692

1✔
693
        let derived_key_id2 = TariKeyId::Derived {
1✔
694
            key: dh_commitment_mask_key_id.clone().into(),
1✔
695
        };
1✔
696
        let dh_encrypted_data_key_id = TariKeyId::DHEncryptedData {
1✔
697
            public_key: CompressedPublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)),
1✔
698
            private_key: managed_key_id.clone().into(),
1✔
699
        };
1✔
700

1✔
701
        let managed_key_id_str = managed_key_id.to_string();
1✔
702
        let imported_key_id_str = imported_key_id.to_string();
1✔
703
        let zero_key_id_str = zero_key_id.to_string();
1✔
704
        let derived_key_id_str = derived_key_id.to_string();
1✔
705
        let derived_key_id_str2 = derived_key_id2.to_string();
1✔
706
        let dh_commitment_mask_key_id_str = dh_commitment_mask_key_id.to_string();
1✔
707
        let dh_encrypted_data_key_id_str = dh_encrypted_data_key_id.to_string();
1✔
708

1✔
709
        assert_eq!(managed_key_id, TariKeyId::from_str(&managed_key_id_str).unwrap());
1✔
710
        assert_eq!(imported_key_id, TariKeyId::from_str(&imported_key_id_str).unwrap());
1✔
711
        assert_eq!(zero_key_id, TariKeyId::from_str(&zero_key_id_str).unwrap());
1✔
712
        assert_eq!(derived_key_id, TariKeyId::from_str(&derived_key_id_str).unwrap());
1✔
713
        assert_eq!(derived_key_id2, TariKeyId::from_str(&derived_key_id_str2).unwrap());
1✔
714
        assert_eq!(
1✔
715
            dh_commitment_mask_key_id,
1✔
716
            TariKeyId::from_str(&dh_commitment_mask_key_id_str).unwrap()
1✔
717
        );
1✔
718
        assert_eq!(
1✔
719
            dh_encrypted_data_key_id,
1✔
720
            TariKeyId::from_str(&dh_encrypted_data_key_id_str).unwrap()
1✔
721
        );
1✔
722
    }
1✔
723
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc