Skip to content

Commit 3074e1f

Browse files
authored
Merge pull request #1959 from mintlayer/fix/wallet-standalone-keys-encryption
fix standalone private keys encryption and usage
2 parents c522764 + 7331ff4 commit 3074e1f

File tree

8 files changed

+224
-73
lines changed

8 files changed

+224
-73
lines changed

test/functional/wallet_tokens_change_metadata_uri.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ async def async_test(self):
133133
assert_in("Success", await wallet.sync())
134134

135135
token_info = node.chainstate_token_info(token_id)
136-
print(token_info)
137136
assert_equal(new_metadata_uri, token_info['content']['metadata_uri']['hex']);
138137
assert token_info['content']['metadata_uri']['text'] is not metadata_uri
139138

wallet/src/account/mod.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,12 +1833,7 @@ impl<K: AccountKeyChains> Account<K> {
18331833

18341834
/// Return true if this destination can be spent by this account
18351835
fn is_destination_mine(&self, destination: &Destination) -> bool {
1836-
match destination {
1837-
Destination::PublicKeyHash(pkh) => self.key_chain.is_public_key_hash_mine(pkh),
1838-
Destination::PublicKey(pk) => self.key_chain.is_public_key_mine(pk),
1839-
Destination::AnyoneCanSpend => false,
1840-
Destination::ScriptHash(_) | Destination::ClassicMultisig(_) => false,
1841-
}
1836+
self.key_chain.has_private_key_for_destination(destination)
18421837
}
18431838

18441839
/// Return true if this destination can be spent by this account or if it is being watched.
@@ -1847,7 +1842,7 @@ impl<K: AccountKeyChains> Account<K> {
18471842
Destination::PublicKeyHash(pkh) => {
18481843
self.key_chain.is_public_key_hash_mine_or_watched(*pkh)
18491844
}
1850-
Destination::PublicKey(pk) => self.key_chain.is_public_key_mine(pk),
1845+
Destination::PublicKey(pk) => self.key_chain.is_public_key_mine_or_watched(pk.clone()),
18511846
Destination::AnyoneCanSpend => false,
18521847
Destination::ScriptHash(_) => false,
18531848
Destination::ClassicMultisig(_) => {
@@ -1886,7 +1881,7 @@ impl<K: AccountKeyChains> Account<K> {
18861881
}
18871882
Destination::PublicKey(pk) => {
18881883
let found = self.key_chain.mark_public_key_as_used(db_tx, &pk)?;
1889-
if found {
1884+
if found || self.key_chain.is_public_key_watched(pk.clone()) {
18901885
return Ok(true);
18911886
}
18921887
}

wallet/src/key_chain/account_key_chain/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,16 @@ impl<V: VrfKeyChain> AccountKeyChains for AccountKeyChainImpl<V> {
471471
self.is_public_key_hash_mine(&pubkey_hash) || self.is_public_key_hash_watched(pubkey_hash)
472472
}
473473

474+
fn is_public_key_watched(&self, public_key: PublicKey) -> bool {
475+
let dest = Destination::PublicKey(public_key);
476+
self.standalone_watch_only_keys.contains_key(&dest)
477+
|| self.standalone_private_keys.contains_key(&dest)
478+
}
479+
480+
fn is_public_key_mine_or_watched(&self, public_key: PublicKey) -> bool {
481+
self.is_public_key_mine(&public_key) || self.is_public_key_watched(public_key)
482+
}
483+
474484
/// Find the corresponding public key for a given public key hash
475485
fn get_public_key_from_public_key_hash(
476486
&self,

wallet/src/key_chain/mod.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,21 @@ where
193193
// Return true if the provided public key belongs to this key chain
194194
fn is_public_key_mine(&self, public_key: &PublicKey) -> bool;
195195

196+
// Return true if the provided public key is one of the standalone added keys
197+
fn is_public_key_watched(&self, public_key: PublicKey) -> bool;
198+
199+
// Return true if the provided public key belongs to this key chain
200+
// or is one of the standalone added keys
201+
fn is_public_key_mine_or_watched(&self, public_key: PublicKey) -> bool;
202+
196203
// Return true if the provided public key hash belongs to this key chain
197204
fn is_public_key_hash_mine(&self, pubkey_hash: &PublicKeyHash) -> bool;
198205

199-
// Return true if the provided public key hash is one the standalone added keys
206+
// Return true if the provided public key hash is one of the standalone added keys
200207
fn is_public_key_hash_watched(&self, pubkey_hash: PublicKeyHash) -> bool;
201208

202209
// Return true if the provided public key hash belongs to this key chain
203-
// or is one the standalone added keys
210+
// or is one of the standalone added keys
204211
fn is_public_key_hash_mine_or_watched(&self, pubkey_hash: PublicKeyHash) -> bool;
205212

206213
/// Find the corresponding public key for a given public key hash

wallet/src/wallet/tests.rs

Lines changed: 169 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -298,33 +298,53 @@ fn create_wallet(chain_config: Arc<ChainConfig>) -> DefaultWallet {
298298
}
299299

300300
#[track_caller]
301-
fn create_block<B, P>(
301+
fn create_block_with_reward_address<B, P>(
302302
chain_config: &Arc<ChainConfig>,
303303
wallet: &mut Wallet<B, P>,
304304
transactions: Vec<SignedTransaction>,
305305
reward: Amount,
306306
block_height: u64,
307-
) -> (Address<Destination>, Block)
307+
address: Destination,
308+
) -> Block
308309
where
309310
B: storage::Backend + 'static,
310311
P: SignerProvider,
311312
{
312-
let address = wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1;
313-
314313
let block1 = Block::new(
315314
transactions,
316315
chain_config.genesis_block_id(),
317316
chain_config.genesis_block().timestamp(),
318317
ConsensusData::None,
319-
BlockReward::new(vec![make_address_output(
320-
address.clone().into_object(),
321-
reward,
322-
)]),
318+
BlockReward::new(vec![make_address_output(address, reward)]),
323319
)
324320
.unwrap();
325321

326322
scan_wallet(wallet, BlockHeight::new(block_height), vec![block1.clone()]);
327-
(address, block1)
323+
block1
324+
}
325+
326+
#[track_caller]
327+
fn create_block<B, P>(
328+
chain_config: &Arc<ChainConfig>,
329+
wallet: &mut Wallet<B, P>,
330+
transactions: Vec<SignedTransaction>,
331+
reward: Amount,
332+
block_height: u64,
333+
) -> (Address<Destination>, Block)
334+
where
335+
B: storage::Backend + 'static,
336+
P: SignerProvider,
337+
{
338+
let address = wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1;
339+
let block = create_block_with_reward_address(
340+
chain_config,
341+
wallet,
342+
transactions,
343+
reward,
344+
block_height,
345+
address.clone().into_object(),
346+
);
347+
(address, block)
328348
}
329349

330350
#[track_caller]
@@ -1251,6 +1271,146 @@ fn locked_wallet_cant_sign_transaction(#[case] seed: Seed) {
12511271
.unwrap();
12521272
}
12531273
}
1274+
1275+
#[rstest]
1276+
#[trace]
1277+
#[case(Seed::from_entropy())]
1278+
fn locked_wallet_standalone_keys(
1279+
#[case] seed: Seed,
1280+
#[values(true, false)] insert_before_encrypt: bool,
1281+
#[values(true, false)] change_password: bool,
1282+
) {
1283+
let mut rng = make_seedable_rng(seed);
1284+
let chain_config = Arc::new(create_mainnet());
1285+
1286+
let mut wallet = create_wallet(chain_config.clone());
1287+
1288+
let coin_balance = get_coin_balance(&wallet);
1289+
assert_eq!(coin_balance, Amount::ZERO);
1290+
1291+
let (standalone_sk, standalone_pk) =
1292+
PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr);
1293+
let mut password = Some(gen_random_password(&mut rng));
1294+
1295+
if insert_before_encrypt {
1296+
wallet
1297+
.add_standalone_private_key(DEFAULT_ACCOUNT_INDEX, standalone_sk, None)
1298+
.unwrap();
1299+
wallet.encrypt_wallet(&password).unwrap();
1300+
} else {
1301+
wallet.encrypt_wallet(&password).unwrap();
1302+
wallet
1303+
.add_standalone_private_key(DEFAULT_ACCOUNT_INDEX, standalone_sk, None)
1304+
.unwrap();
1305+
}
1306+
1307+
if change_password {
1308+
password = Some(gen_random_password(&mut rng));
1309+
wallet.encrypt_wallet(&password).unwrap();
1310+
}
1311+
1312+
let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 1..NETWORK_FEE + 10000));
1313+
1314+
let standalone_destination = if rng.gen::<bool>() {
1315+
Destination::PublicKey(standalone_pk)
1316+
} else {
1317+
Destination::PublicKeyHash((&standalone_pk).into())
1318+
};
1319+
1320+
if rng.gen::<bool>() {
1321+
// test that wallet will recognise a destination belonging to a standalone key in a block
1322+
// reward
1323+
let _ = create_block_with_reward_address(
1324+
&chain_config,
1325+
&mut wallet,
1326+
vec![],
1327+
block1_amount,
1328+
0,
1329+
standalone_destination,
1330+
);
1331+
} else {
1332+
// test that wallet will recognise a destination belonging to a standalone key in a
1333+
// transaction
1334+
let output = make_address_output(standalone_destination, block1_amount);
1335+
1336+
let tx = SignedTransaction::new(Transaction::new(0, vec![], vec![output]).unwrap(), vec![])
1337+
.unwrap();
1338+
1339+
let block1 = Block::new(
1340+
vec![tx.clone()],
1341+
chain_config.genesis_block_id(),
1342+
chain_config.genesis_block().timestamp(),
1343+
ConsensusData::None,
1344+
BlockReward::new(vec![]),
1345+
)
1346+
.unwrap();
1347+
1348+
scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1.clone()]);
1349+
1350+
// check the transaction has been added to the wallet
1351+
let tx_data = wallet
1352+
.get_transaction(DEFAULT_ACCOUNT_INDEX, tx.transaction().get_id())
1353+
.unwrap();
1354+
1355+
assert_eq!(tx_data.get_transaction(), tx.transaction());
1356+
}
1357+
1358+
wallet.lock_wallet().unwrap();
1359+
1360+
// check balance is recognising spendable UTXOs belonging to the standalone private key
1361+
let coin_balance = get_coin_balance(&wallet);
1362+
assert_eq!(coin_balance, block1_amount);
1363+
1364+
// also check utxos
1365+
let utxos = wallet
1366+
.get_utxos(
1367+
DEFAULT_ACCOUNT_INDEX,
1368+
UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::IssueNft,
1369+
UtxoState::Confirmed | UtxoState::Inactive,
1370+
WithLocked::Unlocked,
1371+
)
1372+
.unwrap();
1373+
assert_eq!(utxos.len(), 1);
1374+
1375+
// try to spend the UTXO belonging to the standalone key
1376+
1377+
let new_output = TxOutput::Transfer(
1378+
OutputValue::Coin(Amount::from_atoms(
1379+
rng.gen_range(1..=block1_amount.into_atoms() - NETWORK_FEE),
1380+
)),
1381+
Destination::AnyoneCanSpend,
1382+
);
1383+
1384+
assert_eq!(
1385+
wallet.create_transaction_to_addresses(
1386+
DEFAULT_ACCOUNT_INDEX,
1387+
[new_output.clone()],
1388+
SelectedInputs::Utxos(vec![]),
1389+
BTreeMap::new(),
1390+
FeeRate::from_amount_per_kb(Amount::ZERO),
1391+
FeeRate::from_amount_per_kb(Amount::ZERO),
1392+
TxAdditionalInfo::new(),
1393+
),
1394+
Err(WalletError::DatabaseError(
1395+
wallet_storage::Error::WalletLocked
1396+
))
1397+
);
1398+
1399+
// success after unlock
1400+
wallet.unlock_wallet(&password.unwrap()).unwrap();
1401+
wallet
1402+
.create_transaction_to_addresses(
1403+
DEFAULT_ACCOUNT_INDEX,
1404+
[new_output],
1405+
SelectedInputs::Utxos(vec![]),
1406+
BTreeMap::new(),
1407+
FeeRate::from_amount_per_kb(Amount::ZERO),
1408+
FeeRate::from_amount_per_kb(Amount::ZERO),
1409+
TxAdditionalInfo::new(),
1410+
)
1411+
.unwrap();
1412+
}
1413+
12541414
#[rstest]
12551415
#[trace]
12561416
#[case(Seed::from_entropy())]
@@ -5320,59 +5480,6 @@ fn test_not_exhaustion_of_keys(#[case] seed: Seed) {
53205480
}
53215481
}
53225482

5323-
#[rstest]
5324-
#[trace]
5325-
#[case(Seed::from_entropy())]
5326-
fn test_add_standalone_private_key(#[case] seed: Seed) {
5327-
let mut rng = make_seedable_rng(seed);
5328-
let chain_config = Arc::new(create_regtest());
5329-
5330-
let mut wallet = create_wallet(chain_config.clone());
5331-
5332-
let coin_balance = get_coin_balance(&wallet);
5333-
assert_eq!(coin_balance, Amount::ZERO);
5334-
// generate a random private key unrelated to the wallet and add it
5335-
let (private_key, pub_key) =
5336-
crypto::key::PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr);
5337-
5338-
wallet
5339-
.add_standalone_private_key(DEFAULT_ACCOUNT_INDEX, private_key, None)
5340-
.unwrap();
5341-
5342-
// get the destination address from the new private key and send some coins to it
5343-
let address =
5344-
Address::new(&chain_config, Destination::PublicKeyHash((&pub_key).into())).unwrap();
5345-
5346-
// Generate a new block which sends reward to the new address
5347-
let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000));
5348-
let output = make_address_output(address.clone().into_object(), block1_amount);
5349-
5350-
let tx =
5351-
SignedTransaction::new(Transaction::new(0, vec![], vec![output]).unwrap(), vec![]).unwrap();
5352-
5353-
let block1 = Block::new(
5354-
vec![tx.clone()],
5355-
chain_config.genesis_block_id(),
5356-
chain_config.genesis_block().timestamp(),
5357-
ConsensusData::None,
5358-
BlockReward::new(vec![]),
5359-
)
5360-
.unwrap();
5361-
5362-
scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1.clone()]);
5363-
5364-
// Check amount is still zero
5365-
let coin_balance = get_coin_balance(&wallet);
5366-
assert_eq!(coin_balance, Amount::ZERO);
5367-
5368-
// but the transaction has been added to the wallet
5369-
let tx_data = wallet
5370-
.get_transaction(DEFAULT_ACCOUNT_INDEX, tx.transaction().get_id())
5371-
.unwrap();
5372-
5373-
assert_eq!(tx_data.get_transaction(), tx.transaction());
5374-
}
5375-
53765483
#[rstest]
53775484
#[trace]
53785485
#[case(Seed::from_entropy())]

wallet/storage/src/internal/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ impl<B: storage::Backend> Store<B> {
102102
};
103103
tx.encrypt_root_keys(&sym_key)?;
104104
tx.encrypt_seed_phrase(&sym_key)?;
105+
tx.encrypt_standalone_private_keys(&sym_key)?;
105106
tx.commit()?;
106107

107108
self.encryption_state = EncryptionState::Unlocked(sym_key);

wallet/storage/src/internal/store_tx.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,34 @@ impl<B: storage::Backend> WalletStorageEncryptionWrite for StoreTxRwUnlocked<'_,
690690
.into_iter()
691691
.try_for_each(|(k, v)| self.write::<db::DBSeedPhrase, _, _, _>(k, v))
692692
}
693+
694+
fn encrypt_standalone_private_keys(
695+
&mut self,
696+
new_encryption_key: &Option<SymmetricKey>,
697+
) -> crate::Result<()> {
698+
let encrypted_standalone_private_keys: Vec<_> = self
699+
.storage
700+
.get::<db::DBStandalonePrivateKeys, _>()
701+
.prefix_iter_decoded(&())?
702+
.map(|(k, v)| {
703+
let decrypted = v
704+
.private_key
705+
.try_take(self.encryption_key)
706+
.expect("key was checked when unlocked");
707+
(
708+
k,
709+
StandalonePrivateKey {
710+
label: v.label,
711+
private_key: MaybeEncrypted::new(&decrypted, new_encryption_key),
712+
},
713+
)
714+
})
715+
.collect();
716+
717+
encrypted_standalone_private_keys
718+
.into_iter()
719+
.try_for_each(|(k, v)| self.write::<db::DBStandalonePrivateKeys, _, _, _>(k, v))
720+
}
693721
}
694722

695723
/// Wallet data storage transaction

0 commit comments

Comments
 (0)