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

tari-project / tari / 19227006544

10 Nov 2025 09:31AM UTC coverage: 51.608% (-7.9%) from 59.471%
19227006544

push

github

web-flow
feat: add deterministic transaction id (#7541)

Description
---
Added deterministic transaction IDs, which are an 8-byte (u64) hash
based on the transaction output hash in question and the wallet view
key.
- Any scanned or recovered wallet output will have the same transaction
ID across view or spend wallets.
- Sender wallets will be able to calculate the transaction ID for
receiver wallets if they need to, for that specific output.
- Sender wallets will use their change output as the determining output
hash for the transaction; this will result in the same transaction ID
being allocated upon wallet recovery. In the case of no change output,
the hash of the first ordered output will be used for the transaction
ID.
- For coin split transactions, the hash of the first ordered output will
be used for the transaction ID.

Fixed the issue with the Windows test build target link:
```
: error LNK2019: unresolved external symbol __imp_InitializeSecurityDescriptor referenced in function mdb_env_setup_locks
: error LNK2019: unresolved external symbol __imp_SetSecurityDescriptorDacl referenced in function mdb_env_setup_lock
```

Fixes #7485.

Motivation and Context
---
See #7485.

How Has This Been Tested?
---
Added unit tests.
Performed system-level testing.

What process can a PR reviewer use to test or verify this change?
---
Code review.
System-level testing.

<!-- Checklist -->
<!-- 1. Is the title of your PR in the form that would make nice release
notes? The title, excluding the conventional commit
tag, will be included exactly as is in the CHANGELOG, so please think
about it carefully. -->


Breaking Changes
---

- [x] None
- [ ] Requires data directory on base node to be deleted
- [ ] Requires hard fork
- [ ] Other - Please specify

<!-- Does this include a breaking change? If so, include this line as a
footer -->
<!-- BREAKING CHANGE: Description what the user should do, e.g. delete a
database, resync the chain -->


<!-- This is an auto-generated comm... (continued)

52 of 1260 new or added lines in 14 files covered. (4.13%)

9213 existing lines in 93 files now uncovered.

59188 of 114687 relevant lines covered (51.61%)

8172.79 hits per line

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

18.58
/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;
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)]
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 {
×
83
            KeyManagerBranch::Comms => WALLET_COMMS_AND_SPEND_KEY_BRANCH.to_string(),
×
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.
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 {
148
    pub fn managed_index(&self) -> Option<u64> {
×
149
        match self {
×
150
            TariKeyId::Managed { index, .. } => Some(*index),
×
151
            TariKeyId::Derived { .. } => None,
×
152
            TariKeyId::Imported { .. } => None,
×
153
            TariKeyId::Zero => None,
×
154
            TariKeyId::DHCommitmentMask { .. } => None,
×
155
            TariKeyId::DHEncryptedData { .. } => None,
×
156
            TariKeyId::Encrypted { .. } => None,
×
157
        }
158
    }
×
159

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

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

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

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

UNCOV
221
                    let key = parts.get(1..).expect("Already checked").join(".");
×
UNCOV
222
                    Ok(TariKeyId::Derived {
×
UNCOV
223
                        key: SerializedKeyString::from(key),
×
UNCOV
224
                    })
×
225
                },
UNCOV
226
                DH_COMMITMENT_MASK_BRANCH => {
×
UNCOV
227
                    if parts.len() < 3 {
×
228
                        return Err("Wrong dh_commitment_mask format".to_string());
×
UNCOV
229
                    }
×
UNCOV
230
                    let public_key = CompressedPublicKey::from_hex(parts.get(1).expect("Already checked"))
×
UNCOV
231
                        .map_err(|_| "Invalid public key".to_string())?;
×
UNCOV
232
                    let private_key = parts.get(2..).expect("Already checked").join(".");
×
UNCOV
233
                    Ok(TariKeyId::DHCommitmentMask {
×
UNCOV
234
                        public_key,
×
UNCOV
235
                        private_key: SerializedKeyString::from(private_key),
×
UNCOV
236
                    })
×
237
                },
UNCOV
238
                DH_ENCRYPTED_DATA_BRANCH => {
×
UNCOV
239
                    if parts.len() < 3 {
×
240
                        return Err("Wrong encryted data format".to_string());
×
UNCOV
241
                    }
×
UNCOV
242
                    let public_key = CompressedPublicKey::from_hex(parts.get(1).expect("Already checked"))
×
UNCOV
243
                        .map_err(|_| "Invalid public key".to_string())?;
×
UNCOV
244
                    let private_key = parts.get(2..).expect("Already checked").join(".");
×
UNCOV
245
                    Ok(TariKeyId::DHEncryptedData {
×
UNCOV
246
                        public_key,
×
UNCOV
247
                        private_key: SerializedKeyString::from(private_key),
×
UNCOV
248
                    })
×
249
                },
UNCOV
250
                ENCRYPTED_BRANCH => {
×
UNCOV
251
                    if parts.len() < 3 {
×
252
                        return Err("Wrong encrypted format".to_string());
×
UNCOV
253
                    }
×
UNCOV
254
                    let encrypted: Vec<u8> = from_hex(parts.get(1).expect("Already checked"))
×
UNCOV
255
                        .map_err(|_| "Invalid encrypted bytes".to_string())?;
×
UNCOV
256
                    let key = parts.get(2..).expect("Already checked").join(".");
×
UNCOV
257
                    Ok(TariKeyId::Encrypted {
×
UNCOV
258
                        encrypted,
×
UNCOV
259
                        key: SerializedKeyString::from(key),
×
UNCOV
260
                    })
×
261
                },
262
                _ => Err("Wrong generic format".to_string()),
×
263
            },
264
        }
265
    }
3,988✔
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 {
1,657✔
271
        match self {
1,657✔
272
            TariKeyId::Managed { branch, index } => write!(f, "{MANAGED_KEY_BRANCH}.{branch}.{index}"),
1,652✔
273
            TariKeyId::Derived { key } => write!(f, "{DERIVED_KEY_BRANCH}.{key}"),
5✔
UNCOV
274
            TariKeyId::Imported { key: public_key } => write!(f, "{IMPORTED_KEY_BRANCH}.{public_key}"),
×
UNCOV
275
            TariKeyId::Zero => write!(f, "{ZERO_KEY_BRANCH}"),
×
276
            TariKeyId::DHCommitmentMask {
UNCOV
277
                public_key,
×
UNCOV
278
                private_key,
×
279
            } => {
UNCOV
280
                write!(f, "{DH_COMMITMENT_MASK_BRANCH}.{public_key}.{private_key}")
×
281
            },
282
            TariKeyId::DHEncryptedData {
UNCOV
283
                public_key,
×
UNCOV
284
                private_key,
×
285
            } => {
UNCOV
286
                write!(f, "{DH_ENCRYPTED_DATA_BRANCH}.{public_key}.{private_key}")
×
287
            },
UNCOV
288
            TariKeyId::Encrypted { encrypted, key } => {
×
UNCOV
289
                write!(f, "{ENCRYPTED_BRANCH}.{}.{}", encrypted.to_hex(), key)
×
290
            },
291
        }
292
    }
1,657✔
293
}
294

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

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 {
1,647✔
308
        Self { inner }
1,647✔
309
    }
1,647✔
310
}
311

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

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

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

330
impl From<&TariKeyId> for SerializedKeyString {
331
    fn from(key_id: &TariKeyId) -> Self {
1,041✔
332
        Self::from(key_id.to_string())
1,041✔
333
    }
1,041✔
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>(&mut 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>(&mut self, branch: T)
353
        -> Result<TariKeyAndId, KeyManagerServiceError>;
354

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

586
    async fn generate_burn_claim_signature(
587
        &self,
588
        commitment_mask_key_id: &TariKeyId,
589
        amount: u64,
590
        claim_public_key: &CompressedPublicKey,
591
    ) -> Result<CompressedSignature, TransactionError>;
592

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

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

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

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

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

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

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

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

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

662
    use crate::key_manager::TariKeyId;
663

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

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

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

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

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

UNCOV
710
        assert_eq!(managed_key_id, TariKeyId::from_str(&managed_key_id_str).unwrap());
×
UNCOV
711
        assert_eq!(imported_key_id, TariKeyId::from_str(&imported_key_id_str).unwrap());
×
UNCOV
712
        assert_eq!(zero_key_id, TariKeyId::from_str(&zero_key_id_str).unwrap());
×
UNCOV
713
        assert_eq!(derived_key_id, TariKeyId::from_str(&derived_key_id_str).unwrap());
×
UNCOV
714
        assert_eq!(derived_key_id2, TariKeyId::from_str(&derived_key_id_str2).unwrap());
×
UNCOV
715
        assert_eq!(
×
716
            dh_commitment_mask_key_id,
UNCOV
717
            TariKeyId::from_str(&dh_commitment_mask_key_id_str).unwrap()
×
718
        );
UNCOV
719
        assert_eq!(
×
720
            dh_encrypted_data_key_id,
UNCOV
721
            TariKeyId::from_str(&dh_encrypted_data_key_id_str).unwrap()
×
722
        );
UNCOV
723
    }
×
724
}
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