mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-06-02 07:09:29 +00:00
addressed comments
This commit is contained in:
parent
738dfc0cc4
commit
f4315d1832
@ -155,7 +155,7 @@ wallet account new private
|
||||
# Output:
|
||||
Generated new account with account_id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
||||
With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951
|
||||
With vpk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17
|
||||
With vpk <1184-byte ML-KEM-768 encapsulation key, hex-encoded>
|
||||
```
|
||||
|
||||
> [!Tip]
|
||||
@ -231,19 +231,29 @@ wallet account new private-accounts-key
|
||||
# Output:
|
||||
Generated new private accounts key at path /1
|
||||
With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e
|
||||
With vpk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72
|
||||
With vpk <1184-byte ML-KEM-768 encapsulation key, hex-encoded>
|
||||
```
|
||||
|
||||
> [!Tip]
|
||||
> Ignore the account ID here and use the `npk` and `vpk` values to send to a foreign private account.
|
||||
> [!Important]
|
||||
> The VPK is now a 1184-byte ML-KEM-768 encapsulation key — too large to copy-paste into a command.
|
||||
> The recommended workflow is:
|
||||
>
|
||||
> **Recipient:** export both keys to a single file and send the file to the sender (e.g. as an email attachment):
|
||||
> ```bash
|
||||
> wallet account show-keys --account-id Private/<account-id> > recipient.keys
|
||||
> # Send recipient.keys to the sender out-of-band
|
||||
> ```
|
||||
> The file contains two lines: the npk (hex) on line 1, the vpk (hex) on line 2.
|
||||
>
|
||||
> **Sender:** reference the received file with `--to-keys`:
|
||||
|
||||
### b. Send 3 tokens using the recipient’s npk and vpk
|
||||
### b. Send 3 tokens using the recipient’s keys file
|
||||
|
||||
```bash
|
||||
# The sender has received recipient.keys from the recipient out-of-band
|
||||
wallet auth-transfer send \
|
||||
--from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
|
||||
--to-npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e \
|
||||
--to-vpk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 \
|
||||
--to-keys recipient.keys \
|
||||
--amount 3
|
||||
```
|
||||
|
||||
@ -270,18 +280,19 @@ wallet account new private-accounts-key
|
||||
# Output:
|
||||
Generated new private accounts key at path /2
|
||||
With npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345
|
||||
With vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c
|
||||
With vpk <1184-byte ML-KEM-768 encapsulation key, hex-encoded>
|
||||
```
|
||||
|
||||
Alice shares the `npk` and `vpk` values with Bob and Charlie out of band.
|
||||
|
||||
### b. Bob sends 10 tokens to Alice using identifier 1
|
||||
|
||||
Bob uses the received `alice.keys` file:
|
||||
|
||||
```bash
|
||||
wallet auth-transfer send \
|
||||
--from Public/BobXqJprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPA \
|
||||
--to-npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 \
|
||||
--to-vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c \
|
||||
--to-keys alice.keys \
|
||||
--to-identifier 1 \
|
||||
--amount 10
|
||||
```
|
||||
@ -291,8 +302,7 @@ wallet auth-transfer send \
|
||||
```bash
|
||||
wallet auth-transfer send \
|
||||
--from Public/CharlieYrP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPB \
|
||||
--to-npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 \
|
||||
--to-vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c \
|
||||
--to-keys alice.keys \
|
||||
--to-identifier 2 \
|
||||
--amount 5
|
||||
```
|
||||
|
||||
@ -132,6 +132,7 @@ async fn amm_public() -> Result<()> {
|
||||
to: Some(public_mention(recipient_account_id_1)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 7,
|
||||
};
|
||||
@ -158,6 +159,7 @@ async fn amm_public() -> Result<()> {
|
||||
to: Some(public_mention(recipient_account_id_2)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 7,
|
||||
};
|
||||
@ -530,6 +532,7 @@ async fn amm_new_pool_using_labels() -> Result<()> {
|
||||
to: Some(public_mention(holding_a_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 5,
|
||||
};
|
||||
@ -551,6 +554,7 @@ async fn amm_new_pool_using_labels() -> Result<()> {
|
||||
to: Some(public_mention(holding_b_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 5,
|
||||
};
|
||||
|
||||
@ -260,6 +260,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
|
||||
to: Some(public_mention(sender_ata_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: fund_amount,
|
||||
}),
|
||||
@ -487,6 +488,7 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
|
||||
to: Some(public_mention(sender_ata_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: fund_amount,
|
||||
}),
|
||||
@ -598,6 +600,7 @@ async fn burn_via_ata_private_owner() -> Result<()> {
|
||||
to: Some(public_mention(holder_ata_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: fund_amount,
|
||||
}),
|
||||
|
||||
@ -8,7 +8,10 @@ use integration_tests::{
|
||||
};
|
||||
use log::info;
|
||||
use nssa::{AccountId, program::Program};
|
||||
use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey};
|
||||
use nssa_core::{
|
||||
NullifierPublicKey,
|
||||
encryption::{MlKem768EncapsulationKey, ViewingPublicKey},
|
||||
};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
@ -32,6 +35,7 @@ async fn private_transfer_to_owned_account() -> Result<()> {
|
||||
to: Some(private_mention(to)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -71,7 +75,8 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
|
||||
from: private_mention(from),
|
||||
to: None,
|
||||
to_npk: Some(to_npk_string),
|
||||
to_vpk: Some(hex::encode(to_vpk.0)),
|
||||
to_vpk: Some(hex::encode(to_vpk.to_bytes())),
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -121,6 +126,7 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
|
||||
to: Some(public_mention(to)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -183,7 +189,8 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
|
||||
from: private_mention(from),
|
||||
to: None,
|
||||
to_npk: Some(hex::encode(to.key_chain.nullifier_public_key.0)),
|
||||
to_vpk: Some(hex::encode(&to.key_chain.viewing_public_key.0)),
|
||||
to_vpk: Some(hex::encode(to.key_chain.viewing_public_key.to_bytes())),
|
||||
to_keys: None,
|
||||
to_identifier: Some(to.kind.identifier()),
|
||||
amount: 100,
|
||||
});
|
||||
@ -233,6 +240,7 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> {
|
||||
to: Some(private_mention(to)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -275,7 +283,8 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
|
||||
from: public_mention(from),
|
||||
to: None,
|
||||
to_npk: Some(to_npk_string),
|
||||
to_vpk: Some(hex::encode(to_vpk.0)),
|
||||
to_vpk: Some(hex::encode(to_vpk.to_bytes())),
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -345,7 +354,8 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
|
||||
from: private_mention(from),
|
||||
to: None,
|
||||
to_npk: Some(hex::encode(to.key_chain.nullifier_public_key.0)),
|
||||
to_vpk: Some(hex::encode(&to.key_chain.viewing_public_key.0)),
|
||||
to_vpk: Some(hex::encode(to.key_chain.viewing_public_key.to_bytes())),
|
||||
to_keys: None,
|
||||
to_identifier: Some(to.kind.identifier()),
|
||||
amount: 100,
|
||||
});
|
||||
@ -446,6 +456,7 @@ async fn private_transfer_using_from_label() -> Result<()> {
|
||||
to: Some(private_mention(to)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -539,7 +550,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
|
||||
};
|
||||
|
||||
let npk_hex = hex::encode(npk.0);
|
||||
let vpk_hex = hex::encode(vpk.0);
|
||||
let vpk_hex = hex::encode(vpk.to_bytes());
|
||||
|
||||
let identifier_1 = 1_u128;
|
||||
let identifier_2 = 2_u128;
|
||||
@ -554,6 +565,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
|
||||
to: None,
|
||||
to_npk: Some(npk_hex.clone()),
|
||||
to_vpk: Some(vpk_hex.clone()),
|
||||
to_keys: None,
|
||||
to_identifier: Some(identifier_1),
|
||||
amount: 100,
|
||||
}),
|
||||
@ -567,6 +579,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
|
||||
to: None,
|
||||
to_npk: Some(npk_hex),
|
||||
to_vpk: Some(vpk_hex),
|
||||
to_keys: None,
|
||||
to_identifier: Some(identifier_2),
|
||||
amount: 200,
|
||||
}),
|
||||
@ -654,7 +667,7 @@ async fn ppt_that_chain_calls_faucet_is_dropped() -> Result<()> {
|
||||
let auth_transfer_program_id = Program::authenticated_transfer_program().id();
|
||||
let nsk: nssa_core::NullifierSecretKey = [3; 32];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey(vec![4_u8; 1184]);
|
||||
let vpk = MlKem768EncapsulationKey::from_bytes(vec![4_u8; 1184]).unwrap();
|
||||
let ssk = SharedSecretKey([55_u8; 32]);
|
||||
let epk = EphemeralPublicKey(vec![55_u8; 1088]);
|
||||
let attacker_vault_id = {
|
||||
|
||||
@ -25,6 +25,7 @@ async fn successful_transfer_to_existing_account() -> Result<()> {
|
||||
to: Some(public_mention(ctx.existing_public_accounts()[1])),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -83,6 +84,7 @@ pub async fn successful_transfer_to_new_account() -> Result<()> {
|
||||
to: Some(public_mention(new_persistent_account_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -120,6 +122,7 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> {
|
||||
to: Some(public_mention(ctx.existing_public_accounts()[1])),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 1_000_000,
|
||||
});
|
||||
@ -159,6 +162,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
|
||||
to: Some(public_mention(ctx.existing_public_accounts()[1])),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -192,6 +196,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
|
||||
to: Some(public_mention(ctx.existing_public_accounts()[1])),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -274,6 +279,7 @@ async fn successful_transfer_using_from_label() -> Result<()> {
|
||||
to: Some(public_mention(ctx.existing_public_accounts()[1])),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -319,6 +325,7 @@ async fn successful_transfer_using_to_label() -> Result<()> {
|
||||
to: Some(CliAccountMention::Label(label)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
@ -116,6 +116,7 @@ async fn indexer_state_consistency() -> Result<()> {
|
||||
to: Some(public_mention(ctx.existing_public_accounts()[1])),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -151,6 +152,7 @@ async fn indexer_state_consistency() -> Result<()> {
|
||||
to: Some(private_mention(to)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -234,6 +236,7 @@ async fn indexer_state_consistency_with_labels() -> Result<()> {
|
||||
to: Some(CliAccountMention::Label(to_label)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
@ -194,6 +194,7 @@ fn indexer_ffi_state_consistency() -> Result<()> {
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
});
|
||||
|
||||
@ -233,6 +234,7 @@ fn indexer_ffi_state_consistency() -> Result<()> {
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
});
|
||||
|
||||
@ -344,6 +346,7 @@ fn indexer_ffi_state_consistency_with_labels() -> Result<()> {
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
});
|
||||
|
||||
|
||||
@ -71,7 +71,10 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
|
||||
from: private_mention(from),
|
||||
to: None,
|
||||
to_npk: Some(hex::encode(to_account.key_chain.nullifier_public_key.0)),
|
||||
to_vpk: Some(hex::encode(&to_account.key_chain.viewing_public_key.0)),
|
||||
to_vpk: Some(hex::encode(
|
||||
to_account.key_chain.viewing_public_key.to_bytes(),
|
||||
)),
|
||||
to_keys: None,
|
||||
to_identifier: Some(to_account.kind.identifier()),
|
||||
amount: 100,
|
||||
});
|
||||
@ -147,6 +150,7 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
to: Some(private_mention(to_account_id1)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
@ -158,6 +162,7 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
to: Some(private_mention(to_account_id2)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 101,
|
||||
});
|
||||
@ -197,6 +202,7 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
to: Some(public_mention(to_account_id3)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 102,
|
||||
});
|
||||
@ -208,6 +214,7 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
to: Some(public_mention(to_account_id4)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 103,
|
||||
});
|
||||
@ -268,6 +275,7 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
to: Some(private_mention(to_account_id2)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 10,
|
||||
});
|
||||
@ -278,6 +286,7 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
to: Some(public_mention(to_account_id4)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 11,
|
||||
});
|
||||
|
||||
@ -108,7 +108,9 @@ async fn group_invite_join_key_agreement() -> Result<()> {
|
||||
.sealing_secret_key()
|
||||
.context("Sealing key not found")?;
|
||||
let sealing_pk = key_protocol::key_management::group_key_holder::SealingPublicKey::from_bytes(
|
||||
nssa_core::encryption::ViewingPublicKey::from_seed(&sealing_sk.d, &sealing_sk.z).0,
|
||||
nssa_core::encryption::ViewingPublicKey::from_seed(&sealing_sk.d, &sealing_sk.z)
|
||||
.to_bytes()
|
||||
.to_vec(),
|
||||
);
|
||||
|
||||
let holder = ctx
|
||||
@ -205,6 +207,7 @@ async fn fund_shared_account_from_public() -> Result<()> {
|
||||
to: Some(private_mention(shared_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: None,
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
@ -133,6 +133,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
to: Some(public_mention(recipient_account_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: transfer_amount,
|
||||
};
|
||||
@ -223,6 +224,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
holder: Some(public_mention(recipient_account_id)),
|
||||
holder_npk: None,
|
||||
holder_vpk: None,
|
||||
holder_keys: None,
|
||||
holder_identifier: None,
|
||||
amount: mint_amount,
|
||||
};
|
||||
@ -365,6 +367,7 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
|
||||
to: Some(private_mention(recipient_account_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: transfer_amount,
|
||||
};
|
||||
@ -554,6 +557,7 @@ async fn create_token_with_private_definition() -> Result<()> {
|
||||
holder: Some(public_mention(recipient_account_id_public)),
|
||||
holder_npk: None,
|
||||
holder_vpk: None,
|
||||
holder_keys: None,
|
||||
holder_identifier: None,
|
||||
amount: mint_amount_public,
|
||||
};
|
||||
@ -601,6 +605,7 @@ async fn create_token_with_private_definition() -> Result<()> {
|
||||
holder: Some(private_mention(recipient_account_id_private)),
|
||||
holder_npk: None,
|
||||
holder_vpk: None,
|
||||
holder_keys: None,
|
||||
holder_identifier: None,
|
||||
amount: mint_amount_private,
|
||||
};
|
||||
@ -740,6 +745,7 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
|
||||
to: Some(private_mention(recipient_account_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: transfer_amount,
|
||||
};
|
||||
@ -868,6 +874,7 @@ async fn shielded_token_transfer() -> Result<()> {
|
||||
to: Some(private_mention(recipient_account_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: transfer_amount,
|
||||
};
|
||||
@ -991,6 +998,7 @@ async fn deshielded_token_transfer() -> Result<()> {
|
||||
to: Some(public_mention(recipient_account_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: transfer_amount,
|
||||
};
|
||||
@ -1124,7 +1132,8 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
|
||||
definition: private_mention(definition_account_id),
|
||||
holder: None,
|
||||
holder_npk: Some(hex::encode(holder_keys.nullifier_public_key.0)),
|
||||
holder_vpk: Some(hex::encode(&holder_keys.viewing_public_key.0)),
|
||||
holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.to_bytes())),
|
||||
holder_keys: None,
|
||||
holder_identifier: Some(holder_identifier),
|
||||
amount: mint_amount,
|
||||
};
|
||||
@ -1323,6 +1332,7 @@ async fn transfer_token_using_from_label() -> Result<()> {
|
||||
to: Some(public_mention(recipient_account_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: transfer_amount,
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _};
|
||||
use nssa_core::{
|
||||
SharedSecretKey,
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
encryption::{EphemeralPublicKey, MlKem768EncapsulationKey},
|
||||
program::{PdaSeed, ProgramId},
|
||||
};
|
||||
use rand::{RngCore as _, rngs::OsRng};
|
||||
@ -156,8 +156,9 @@ impl GroupKeyHolder {
|
||||
/// different ciphertexts.
|
||||
#[must_use]
|
||||
pub fn seal_for(&self, recipient_key: &SealingPublicKey) -> Vec<u8> {
|
||||
let vpk = ViewingPublicKey(recipient_key.0.clone());
|
||||
let (shared, kem_ct) = SharedSecretKey::encapsulate(&vpk);
|
||||
let sealing_key = MlKem768EncapsulationKey::from_bytes(recipient_key.0.clone())
|
||||
.expect("key_protocol::group_key_holder::GroupKeyHolder::seal_for: SealingPublicKey must be a valid ML-KEM-768 encapsulation key");
|
||||
let (shared, kem_ct) = SharedSecretKey::encapsulate(&sealing_key);
|
||||
let aes_key = Self::seal_kdf(&shared);
|
||||
let cipher = Aes256Gcm::new(&aes_key.into());
|
||||
|
||||
@ -404,7 +405,9 @@ mod tests {
|
||||
let recipient_vpk = recipient_keys.generate_viewing_public_key();
|
||||
let recipient_vsk = recipient_keys.viewing_secret_key;
|
||||
|
||||
let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0));
|
||||
let sealed = holder.seal_for(&SealingPublicKey::from_bytes(
|
||||
recipient_vpk.to_bytes().to_vec(),
|
||||
));
|
||||
let restored = GroupKeyHolder::unseal(&sealed, &recipient_vsk).expect("unseal");
|
||||
|
||||
assert_eq!(restored.dangerous_raw_gms(), holder.dangerous_raw_gms());
|
||||
@ -434,7 +437,9 @@ mod tests {
|
||||
.produce_private_key_holder(None)
|
||||
.viewing_secret_key;
|
||||
|
||||
let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0));
|
||||
let sealed = holder.seal_for(&SealingPublicKey::from_bytes(
|
||||
recipient_vpk.to_bytes().to_vec(),
|
||||
));
|
||||
let result = GroupKeyHolder::unseal(&sealed, &wrong_vsk);
|
||||
assert!(matches!(result, Err(super::SealError::DecryptionFailed)));
|
||||
}
|
||||
@ -449,7 +454,9 @@ mod tests {
|
||||
let recipient_vpk = recipient_keys.generate_viewing_public_key();
|
||||
let recipient_vsk = recipient_keys.viewing_secret_key;
|
||||
|
||||
let mut sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0));
|
||||
let mut sealed = holder.seal_for(&SealingPublicKey::from_bytes(
|
||||
recipient_vpk.to_bytes().to_vec(),
|
||||
));
|
||||
// Flip a byte in the AES-GCM ciphertext portion (after KEM ciphertext + nonce).
|
||||
let last = sealed.len() - 1;
|
||||
sealed[last] ^= 0xFF;
|
||||
@ -468,7 +475,7 @@ mod tests {
|
||||
.produce_private_key_holder(None)
|
||||
.generate_viewing_public_key();
|
||||
|
||||
let sealing_key = SealingPublicKey::from_bytes(recipient_vpk.0);
|
||||
let sealing_key = SealingPublicKey::from_bytes(recipient_vpk.to_bytes().to_vec());
|
||||
let sealed_a = holder.seal_for(&sealing_key);
|
||||
let sealed_b = holder.seal_for(&sealing_key);
|
||||
assert_ne!(sealed_a, sealed_b);
|
||||
@ -531,7 +538,8 @@ mod tests {
|
||||
let bob_vpk = bob_keys.generate_viewing_public_key();
|
||||
let bob_vsk = bob_keys.viewing_secret_key;
|
||||
|
||||
let sealed = alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.0));
|
||||
let sealed =
|
||||
alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.to_bytes().to_vec()));
|
||||
let bob_holder =
|
||||
GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS");
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use nssa_core::{NullifierPublicKey, PrivateAccountKind, encryption::ViewingPublicKey};
|
||||
use nssa_core::{NullifierPublicKey, PrivateAccountKind, encryption::MlKem768EncapsulationKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Digest as _;
|
||||
|
||||
@ -37,7 +37,7 @@ impl ChildKeysPrivate {
|
||||
let vsk = ssk.generate_viewing_secret_seed_key(None);
|
||||
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from(&vsk);
|
||||
let vpk = MlKem768EncapsulationKey::from(&vsk);
|
||||
|
||||
Self {
|
||||
value: (
|
||||
@ -59,15 +59,17 @@ impl ChildKeysPrivate {
|
||||
|
||||
#[must_use]
|
||||
pub fn nth_child(&self, cci: u32) -> Self {
|
||||
// `parent_hash`` is used to incorporate entropy based on the parent node's keys
|
||||
// to generate the `ssk` and `ccc` values.
|
||||
let mut parent_hash = sha2::Sha256::new();
|
||||
parent_hash.update(b"LEE/keys");
|
||||
parent_hash.update([0_u8; 16]);
|
||||
parent_hash.update([9_u8]);
|
||||
parent_hash.update(self.value.0.private_key_holder.nullifier_secret_key);
|
||||
parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.d);
|
||||
parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.z);
|
||||
let parent_pt = parent_hash.finalize();
|
||||
|
||||
// Each child (of the same parent node) share the same `parent_pt`.
|
||||
// To ensure that each child generates unique keys, we include the child index.
|
||||
let mut input = vec![];
|
||||
input.extend_from_slice(b"LEE_seed_priv");
|
||||
input.extend_from_slice(&parent_pt);
|
||||
@ -89,7 +91,7 @@ impl ChildKeysPrivate {
|
||||
let vsk = ssk.generate_viewing_secret_seed_key(Some(cci));
|
||||
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from(&vsk);
|
||||
let vpk = MlKem768EncapsulationKey::from(&vsk);
|
||||
|
||||
Self {
|
||||
value: (
|
||||
@ -166,16 +168,16 @@ mod tests {
|
||||
247, 155, 113, 122, 246, 192, 0, 70, 61, 76, 71, 70, 2,
|
||||
]);
|
||||
|
||||
let expected_vsk: ViewingSecretKey = ViewingSecretKey {
|
||||
d: [
|
||||
let expected_vsk = ViewingSecretKey::new(
|
||||
[
|
||||
187, 143, 146, 12, 68, 148, 25, 203, 21, 92, 131, 2, 221, 81, 117, 62, 98, 194,
|
||||
159, 177, 102, 254, 236, 182, 76, 242, 116, 219, 17, 166, 99, 36,
|
||||
],
|
||||
z: [
|
||||
[
|
||||
80, 97, 83, 209, 145, 99, 168, 99, 89, 29, 153, 236, 82, 99, 134, 114, 168, 19,
|
||||
223, 69, 34, 47, 76, 76, 15, 97, 245, 184, 25, 103, 251, 82,
|
||||
],
|
||||
};
|
||||
);
|
||||
|
||||
let expected_vpk: [u8; 1184] = [
|
||||
127, 229, 162, 212, 104, 117, 4, 150, 192, 103, 122, 195, 14, 35, 12, 60, 52, 23, 220,
|
||||
@ -280,16 +282,16 @@ mod tests {
|
||||
219, 114, 113, 16, 42, 27, 220, 96, 151, 124, 8, 65,
|
||||
]);
|
||||
|
||||
let expected_vsk: ViewingSecretKey = ViewingSecretKey {
|
||||
d: [
|
||||
let expected_vsk = ViewingSecretKey::new(
|
||||
[
|
||||
81, 154, 68, 152, 72, 163, 82, 17, 125, 156, 193, 135, 129, 93, 227, 55, 224, 104,
|
||||
119, 232, 13, 101, 241, 20, 175, 72, 192, 186, 176, 246, 140, 211,
|
||||
],
|
||||
z: [
|
||||
[
|
||||
31, 40, 109, 41, 185, 61, 173, 79, 102, 171, 158, 245, 232, 71, 57, 157, 142, 117,
|
||||
184, 235, 216, 71, 55, 44, 33, 156, 167, 133, 184, 92, 47, 174,
|
||||
],
|
||||
};
|
||||
);
|
||||
|
||||
let expected_vpk: [u8; 1184] = [
|
||||
67, 150, 145, 133, 41, 124, 194, 102, 104, 131, 195, 8, 168, 170, 200, 40, 210, 84, 85,
|
||||
|
||||
@ -106,6 +106,27 @@ mod tests {
|
||||
let _shared_secret = account_id_key_holder.calculate_shared_secret_receiver(&epk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_shared_secret_receiver_returns_none_for_malformed_epk() {
|
||||
let key_chain = KeyChain::new_os_random();
|
||||
|
||||
let short_epk = EphemeralPublicKey(vec![42_u8; 100]);
|
||||
assert!(
|
||||
key_chain
|
||||
.calculate_shared_secret_receiver(&short_epk)
|
||||
.is_none(),
|
||||
"short EphemeralPublicKey must return None"
|
||||
);
|
||||
|
||||
let long_epk = EphemeralPublicKey(vec![42_u8; 1089]);
|
||||
assert!(
|
||||
key_chain
|
||||
.calculate_shared_secret_receiver(&long_epk)
|
||||
.is_none(),
|
||||
"long EphemeralPublicKey must return None"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_generation_test() {
|
||||
let seed_holder = SeedHolder::new_os_random();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use bip39::Mnemonic;
|
||||
use common::HashType;
|
||||
use ml_kem;
|
||||
use nssa_core::{NullifierPublicKey, NullifierSecretKey, encryption::ViewingPublicKey};
|
||||
use nssa_core::{NullifierPublicKey, NullifierSecretKey, encryption::MlKem768EncapsulationKey};
|
||||
use rand::{RngCore as _, rngs::OsRng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest as _, digest::FixedOutput as _};
|
||||
@ -25,6 +25,13 @@ pub struct ViewingSecretKey {
|
||||
pub z: [u8; 32],
|
||||
}
|
||||
|
||||
impl ViewingSecretKey {
|
||||
#[must_use]
|
||||
pub const fn new(d: [u8; 32], z: [u8; 32]) -> Self {
|
||||
Self { d, z }
|
||||
}
|
||||
}
|
||||
|
||||
/// Private key holder. Produces public keys. Can produce `account_id`. Can produce shared secret
|
||||
/// for recepient.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
@ -139,22 +146,22 @@ impl SecretSpendingKey {
|
||||
|
||||
let full_seed = hmac_sha512::HMAC::mac(bytes, b"LEE_viewing_seed");
|
||||
|
||||
ViewingSecretKey {
|
||||
d: *full_seed
|
||||
ViewingSecretKey::new(
|
||||
*full_seed
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
z: *full_seed
|
||||
*full_seed
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32"),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn generate_viewing_secret_key(seed: [u8; 64]) -> ViewingSecretKey {
|
||||
ViewingSecretKey {
|
||||
d: *seed.first_chunk::<32>().expect("seed is 64 bytes"),
|
||||
z: *seed.last_chunk::<32>().expect("seed is 64 bytes"),
|
||||
}
|
||||
ViewingSecretKey::new(
|
||||
*seed.first_chunk::<32>().expect("seed is 64 bytes"),
|
||||
*seed.last_chunk::<32>().expect("seed is 64 bytes"),
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@ -166,14 +173,15 @@ impl SecretSpendingKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ViewingSecretKey> for ViewingPublicKey {
|
||||
impl From<&ViewingSecretKey> for MlKem768EncapsulationKey {
|
||||
fn from(sk: &ViewingSecretKey) -> Self {
|
||||
use ml_kem::{Kem, KeyExport as _, MlKem768, Seed};
|
||||
let mut seed_bytes = [0_u8; 64];
|
||||
seed_bytes[..32].copy_from_slice(&sk.d);
|
||||
seed_bytes[32..].copy_from_slice(&sk.z);
|
||||
let dk = <MlKem768 as Kem>::DecapsulationKey::from_seed(Seed::from(seed_bytes));
|
||||
Self(dk.encapsulation_key().to_bytes().to_vec())
|
||||
Self::from_bytes(dk.encapsulation_key().to_bytes().to_vec())
|
||||
.expect("key_protocol::secret_holders::From<&ViewingSecretKey>: ML-KEM-768 encapsulation key is always 1184 bytes")
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,8 +192,8 @@ impl PrivateKeyHolder {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn generate_viewing_public_key(&self) -> ViewingPublicKey {
|
||||
ViewingPublicKey::from(&self.viewing_secret_key)
|
||||
pub fn generate_viewing_public_key(&self) -> MlKem768EncapsulationKey {
|
||||
MlKem768EncapsulationKey::from(&self.viewing_secret_key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ risc0-build = "3.0.3"
|
||||
risc0-binfmt = "3.0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
nssa_core = { workspace = true, features = ["test_utils"] }
|
||||
token_core.workspace = true
|
||||
authenticated_transfer_core.workspace = true
|
||||
test_program_methods.workspace = true
|
||||
|
||||
@ -25,3 +25,4 @@ serde_json.workspace = true
|
||||
[features]
|
||||
default = []
|
||||
host = ["dep:ml-kem"]
|
||||
test_utils = ["host"]
|
||||
|
||||
@ -6,7 +6,7 @@ use chacha20::{
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "host")]
|
||||
pub use shared_key_derivation::{EphemeralPublicKey, ViewingPublicKey};
|
||||
pub use shared_key_derivation::{EphemeralPublicKey, MlKem768EncapsulationKey, ViewingPublicKey};
|
||||
|
||||
use crate::{Commitment, account::Account, program::PrivateAccountKind};
|
||||
#[cfg(feature = "host")]
|
||||
|
||||
@ -22,17 +22,32 @@ pub struct EphemeralPublicKey(pub Vec<u8>);
|
||||
BorshSerialize,
|
||||
BorshDeserialize,
|
||||
)]
|
||||
pub struct ViewingPublicKey(pub Vec<u8>);
|
||||
pub struct MlKem768EncapsulationKey(Vec<u8>);
|
||||
|
||||
pub type ViewingPublicKey = MlKem768EncapsulationKey;
|
||||
|
||||
impl MlKem768EncapsulationKey {
|
||||
/// Expected byte length of an ML-KEM-768 encapsulation key.
|
||||
pub const LEN: usize = 1184;
|
||||
|
||||
/// Construct from raw bytes, returning an error if the length is not [`Self::LEN`].
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, crate::error::NssaCoreError> {
|
||||
if bytes.len() != Self::LEN {
|
||||
return Err(crate::error::NssaCoreError::DeserializationError(format!(
|
||||
"MlKem768EncapsulationKey must be {} bytes, got {}",
|
||||
Self::LEN,
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
Ok(Self(bytes))
|
||||
}
|
||||
|
||||
impl ViewingPublicKey {
|
||||
#[must_use]
|
||||
pub fn to_bytes(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Derive the ML-KEM-768 encapsulation key from the FIPS 203 seed halves `d` and `z`.
|
||||
/// Allows any crate to construct a VPK from raw seed bytes without importing
|
||||
/// `key_protocol::ViewingSecretKey`.
|
||||
#[must_use]
|
||||
pub fn from_seed(d: &[u8; 32], z: &[u8; 32]) -> Self {
|
||||
let mut seed = Seed::default();
|
||||
@ -44,21 +59,21 @@ impl ViewingPublicKey {
|
||||
}
|
||||
|
||||
impl SharedSecretKey {
|
||||
/// Sender: encapsulate a fresh shared secret toward `vpk`.
|
||||
/// Sender: encapsulate a fresh shared secret toward `ek`.
|
||||
///
|
||||
/// Returns `(shared_secret, ciphertext)`. The ciphertext must be included in the transaction
|
||||
/// as the `EphemeralPublicKey`; the receiver recovers the same shared secret via
|
||||
/// [`decapsulate`][Self::decapsulate].
|
||||
/// [`Self::decapsulate`].
|
||||
#[must_use]
|
||||
pub fn encapsulate(vpk: &ViewingPublicKey) -> (Self, EphemeralPublicKey) {
|
||||
let ek_bytes: ml_kem::kem::Key<ml_kem::EncapsulationKey768> = vpk
|
||||
.0
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.expect("ViewingPublicKey must be 1184 bytes (ML-KEM-768 encapsulation key)");
|
||||
let ek = ml_kem::EncapsulationKey768::new(&ek_bytes)
|
||||
.expect("ViewingPublicKey bytes must encode a valid ML-KEM-768 encapsulation key");
|
||||
let (ct, ss) = ek.encapsulate();
|
||||
pub fn encapsulate(ek: &MlKem768EncapsulationKey) -> (Self, EphemeralPublicKey) {
|
||||
let ek_bytes: ml_kem::kem::Key<ml_kem::EncapsulationKey768> =
|
||||
ek.0.as_slice()
|
||||
.try_into()
|
||||
.expect("MlKem768EncapsulationKey must be 1184 bytes");
|
||||
let ek_obj = ml_kem::EncapsulationKey768::new(&ek_bytes).expect(
|
||||
"MlKem768EncapsulationKey bytes must encode a valid ML-KEM-768 encapsulation key",
|
||||
);
|
||||
let (ct, ss) = ek_obj.encapsulate();
|
||||
let ss_bytes: [u8; 32] = ss
|
||||
.as_slice()
|
||||
.try_into()
|
||||
@ -66,15 +81,19 @@ impl SharedSecretKey {
|
||||
(Self(ss_bytes), EphemeralPublicKey(ct.to_vec()))
|
||||
}
|
||||
|
||||
/// Sender: deterministically encapsulate a shared secret toward `vpk`.
|
||||
/// Deterministically encapsulate a shared secret toward `ek` for use in tests.
|
||||
///
|
||||
/// The KEM randomness is derived as `SHA-256(message_hash || output_index_le)`,
|
||||
/// making the ciphertext reproducible from the same `(vpk, message_hash, output_index)`
|
||||
/// triple. Use a distinct `output_index` for each private account output in the same
|
||||
/// transaction to ensure per-output EPK uniqueness.
|
||||
/// The shared secret has no secret entropy — it is fully determined by `ek`,
|
||||
/// `message_hash`, and `output_index`, all of which are public. This makes it
|
||||
/// unsuitable for real encryption but useful for producing stable, reproducible
|
||||
/// shared secrets in unit tests. Use a distinct `output_index` per output to
|
||||
/// avoid EPK collisions across multiple outputs in the same test.
|
||||
///
|
||||
/// For production use [`Self::encapsulate`], which draws randomness from the OS.
|
||||
#[cfg(any(test, feature = "test_utils"))]
|
||||
#[must_use]
|
||||
pub fn encapsulate_deterministic(
|
||||
vpk: &ViewingPublicKey,
|
||||
ek: &MlKem768EncapsulationKey,
|
||||
message_hash: &[u8; 32],
|
||||
output_index: u32,
|
||||
) -> (Self, EphemeralPublicKey) {
|
||||
@ -87,14 +106,14 @@ impl SharedSecretKey {
|
||||
let m: ml_kem::B32 =
|
||||
ml_kem::array::Array::try_from(hash.as_bytes()).expect("SHA-256 output is 32 bytes");
|
||||
|
||||
let ek_bytes: ml_kem::kem::Key<ml_kem::EncapsulationKey768> = vpk
|
||||
.0
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.expect("ViewingPublicKey must be 1184 bytes (ML-KEM-768 encapsulation key)");
|
||||
let ek = ml_kem::EncapsulationKey768::new(&ek_bytes)
|
||||
.expect("ViewingPublicKey bytes must encode a valid ML-KEM-768 encapsulation key");
|
||||
let (ct, ss) = ek.encapsulate_deterministic(&m);
|
||||
let ek_bytes: ml_kem::kem::Key<ml_kem::EncapsulationKey768> =
|
||||
ek.0.as_slice()
|
||||
.try_into()
|
||||
.expect("MlKem768EncapsulationKey must be 1184 bytes");
|
||||
let ek_obj = ml_kem::EncapsulationKey768::new(&ek_bytes).expect(
|
||||
"MlKem768EncapsulationKey bytes must encode a valid ML-KEM-768 encapsulation key",
|
||||
);
|
||||
let (ct, ss) = ek_obj.encapsulate_deterministic(&m);
|
||||
let ss_bytes: [u8; 32] = ss
|
||||
.as_slice()
|
||||
.try_into()
|
||||
@ -109,7 +128,11 @@ impl SharedSecretKey {
|
||||
///
|
||||
/// `d` and `z` are the two 32-byte halves of the FIPS 203 `ViewingSecretKey` seed.
|
||||
#[must_use]
|
||||
pub fn decapsulate(ciphertext: &EphemeralPublicKey, d: &[u8; 32], z: &[u8; 32]) -> Option<Self> {
|
||||
pub fn decapsulate(
|
||||
ciphertext: &EphemeralPublicKey,
|
||||
d: &[u8; 32],
|
||||
z: &[u8; 32],
|
||||
) -> Option<Self> {
|
||||
let mut seed = Seed::default();
|
||||
seed[..32].copy_from_slice(d);
|
||||
seed[32..].copy_from_slice(z);
|
||||
@ -140,15 +163,15 @@ mod tests {
|
||||
|
||||
let dk = ml_kem::DecapsulationKey768::from_seed(seed);
|
||||
let ek_bytes = dk.encapsulation_key().to_bytes();
|
||||
let vpk = ViewingPublicKey(ek_bytes.to_vec());
|
||||
let ek = MlKem768EncapsulationKey(ek_bytes.to_vec());
|
||||
|
||||
let (sender_ss, epk) = SharedSecretKey::encapsulate(&vpk);
|
||||
let (sender_ss, epk) = SharedSecretKey::encapsulate(&ek);
|
||||
let receiver_ss = SharedSecretKey::decapsulate(&epk, &d, &z).unwrap();
|
||||
|
||||
assert_eq!(sender_ss.0, receiver_ss.0, "shared secrets must match");
|
||||
assert_eq!(epk.0.len(), 1088, "ML-KEM-768 ciphertext is 1088 bytes");
|
||||
assert_eq!(
|
||||
vpk.0.len(),
|
||||
ek.0.len(),
|
||||
1184,
|
||||
"ML-KEM-768 encapsulation key is 1184 bytes"
|
||||
);
|
||||
@ -186,23 +209,23 @@ mod tests {
|
||||
let (d1, z1) = ([1_u8; 32], [2_u8; 32]);
|
||||
let (d2, z2) = ([3_u8; 32], [4_u8; 32]);
|
||||
|
||||
let vpk1 = {
|
||||
let ek1 = {
|
||||
let mut seed = Seed::default();
|
||||
seed[..32].copy_from_slice(&d1);
|
||||
seed[32..].copy_from_slice(&z1);
|
||||
let dk = ml_kem::DecapsulationKey768::from_seed(seed);
|
||||
ViewingPublicKey(dk.encapsulation_key().to_bytes().to_vec())
|
||||
MlKem768EncapsulationKey(dk.encapsulation_key().to_bytes().to_vec())
|
||||
};
|
||||
let vpk2 = {
|
||||
let ek2 = {
|
||||
let mut seed = Seed::default();
|
||||
seed[..32].copy_from_slice(&d2);
|
||||
seed[32..].copy_from_slice(&z2);
|
||||
let dk = ml_kem::DecapsulationKey768::from_seed(seed);
|
||||
ViewingPublicKey(dk.encapsulation_key().to_bytes().to_vec())
|
||||
MlKem768EncapsulationKey(dk.encapsulation_key().to_bytes().to_vec())
|
||||
};
|
||||
|
||||
let (ss1, _) = SharedSecretKey::encapsulate(&vpk1);
|
||||
let (ss2, _) = SharedSecretKey::encapsulate(&vpk2);
|
||||
let (ss1, _) = SharedSecretKey::encapsulate(&ek1);
|
||||
let (ss2, _) = SharedSecretKey::encapsulate(&ek2);
|
||||
|
||||
assert_ne!(ss1.0, ss2.0);
|
||||
}
|
||||
|
||||
@ -136,10 +136,7 @@ pub fn initial_priv_accounts_private_keys() -> Vec<PrivateAccountPrivateInitialD
|
||||
secret_spending_key: SecretSpendingKey(SSK_PRIV_ACC_A),
|
||||
private_key_holder: PrivateKeyHolder {
|
||||
nullifier_secret_key: NSK_PRIV_ACC_A,
|
||||
viewing_secret_key: ViewingSecretKey {
|
||||
d: VSK_D_PRIV_ACC_A,
|
||||
z: VSK_Z_PRIV_ACC_A,
|
||||
},
|
||||
viewing_secret_key: ViewingSecretKey::new(VSK_D_PRIV_ACC_A, VSK_Z_PRIV_ACC_A),
|
||||
},
|
||||
nullifier_public_key: NullifierPublicKey(NPK_PRIV_ACC_A),
|
||||
viewing_public_key: ViewingPublicKey::from_seed(&VSK_D_PRIV_ACC_A, &VSK_Z_PRIV_ACC_A),
|
||||
@ -149,10 +146,7 @@ pub fn initial_priv_accounts_private_keys() -> Vec<PrivateAccountPrivateInitialD
|
||||
secret_spending_key: SecretSpendingKey(SSK_PRIV_ACC_B),
|
||||
private_key_holder: PrivateKeyHolder {
|
||||
nullifier_secret_key: NSK_PRIV_ACC_B,
|
||||
viewing_secret_key: ViewingSecretKey {
|
||||
d: VSK_D_PRIV_ACC_B,
|
||||
z: VSK_Z_PRIV_ACC_B,
|
||||
},
|
||||
viewing_secret_key: ViewingSecretKey::new(VSK_D_PRIV_ACC_B, VSK_Z_PRIV_ACC_B),
|
||||
},
|
||||
nullifier_public_key: NullifierPublicKey(NPK_PRIV_ACC_B),
|
||||
viewing_public_key: ViewingPublicKey::from_seed(&VSK_D_PRIV_ACC_B, &VSK_Z_PRIV_ACC_B),
|
||||
|
||||
@ -180,6 +180,7 @@ async fn timed_token_send(
|
||||
to: Some(public_mention(to_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount,
|
||||
}),
|
||||
|
||||
@ -50,6 +50,7 @@ pub async fn run(ctx: &mut TestContext) -> Result<ScenarioOutput> {
|
||||
to: Some(public_mention(recipient_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: AMOUNT_PER_TRANSFER,
|
||||
}),
|
||||
|
||||
@ -69,6 +69,7 @@ pub async fn run(ctx: &mut TestContext) -> Result<ScenarioOutput> {
|
||||
to: Some(public_mention(sender_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: AMOUNT_PER_TRANSFER * 5,
|
||||
}),
|
||||
@ -97,6 +98,7 @@ pub async fn run(ctx: &mut TestContext) -> Result<ScenarioOutput> {
|
||||
to: Some(public_mention(*recipient_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: AMOUNT_PER_TRANSFER,
|
||||
}),
|
||||
|
||||
@ -46,6 +46,7 @@ pub async fn run(ctx: &mut TestContext) -> Result<ScenarioOutput> {
|
||||
to: Some(private_mention(private_a)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 1_000,
|
||||
}),
|
||||
@ -64,6 +65,7 @@ pub async fn run(ctx: &mut TestContext) -> Result<ScenarioOutput> {
|
||||
to: Some(public_mention(public_recipient_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
}),
|
||||
@ -82,6 +84,7 @@ pub async fn run(ctx: &mut TestContext) -> Result<ScenarioOutput> {
|
||||
to: Some(private_mention(private_b)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 200,
|
||||
}),
|
||||
|
||||
@ -41,6 +41,7 @@ pub async fn run(ctx: &mut TestContext) -> Result<ScenarioOutput> {
|
||||
to: Some(public_mention(recipient_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 1_000,
|
||||
}),
|
||||
@ -61,6 +62,7 @@ pub async fn run(ctx: &mut TestContext) -> Result<ScenarioOutput> {
|
||||
to: Some(private_mention(private_recipient_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 500,
|
||||
}),
|
||||
|
||||
@ -164,7 +164,11 @@ impl FfiPrivateAccountKeys {
|
||||
let slice = unsafe {
|
||||
slice::from_raw_parts(self.viewing_public_key, self.viewing_public_key_len)
|
||||
};
|
||||
Ok(nssa_core::encryption::ViewingPublicKey(slice.to_vec()))
|
||||
Ok(
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::from_bytes(slice.to_vec()).expect(
|
||||
"wallet_ffi::types::FfiPrivateAccountKeys::vpk: length already validated above",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Err(WalletFfiError::InvalidKeyValue)
|
||||
}
|
||||
|
||||
@ -51,6 +51,20 @@ pub enum AccountSubcommand {
|
||||
/// Import external account.
|
||||
#[command(subcommand)]
|
||||
Import(ImportSubcommand),
|
||||
/// Print the npk and vpk for a private account, one per line.
|
||||
///
|
||||
/// Outputs two lines: npk (hex) then vpk (hex). Save to a file and share it
|
||||
/// with senders so they can reference it with `--to-keys /path/to/file`.
|
||||
///
|
||||
/// ```text
|
||||
/// wallet account show-keys --account-id Private/... > alice.keys
|
||||
/// ```
|
||||
#[command(name = "show-keys")]
|
||||
ShowKeys {
|
||||
/// Either 32 byte base58 account id string with privacy prefix or a label.
|
||||
#[arg(long)]
|
||||
account_id: CliAccountMention,
|
||||
},
|
||||
}
|
||||
|
||||
/// Represents generic register CLI subcommand.
|
||||
@ -225,7 +239,7 @@ impl WalletSubcommand for NewSubcommand {
|
||||
println!("Shared account from group '{group}'");
|
||||
println!("AccountId: Private/{}", info.account_id);
|
||||
println!("NPK: {}", hex::encode(info.npk.0));
|
||||
println!("VPK: {}", hex::encode(&info.vpk.0));
|
||||
println!("VPK: {}", hex::encode(info.vpk.to_bytes()));
|
||||
|
||||
wallet_core.store_persistent_data()?;
|
||||
Ok(SubcommandReturnValue::RegisterAccount {
|
||||
@ -419,6 +433,25 @@ impl WalletSubcommand for AccountSubcommand {
|
||||
Self::Import(import_subcommand) => {
|
||||
import_subcommand.handle_subcommand(wallet_core).await
|
||||
}
|
||||
Self::ShowKeys { account_id } => {
|
||||
let resolved = account_id.resolve(wallet_core.storage())?;
|
||||
let AccountIdWithPrivacy::Private(account_id) = resolved else {
|
||||
anyhow::bail!(
|
||||
"wallet::cli::account::AccountSubcommand::ShowKeys: show-keys is only available for private accounts"
|
||||
);
|
||||
};
|
||||
let entry = wallet_core
|
||||
.storage()
|
||||
.key_chain()
|
||||
.private_account(account_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("wallet::cli::account::AccountSubcommand::ShowKeys: private account not found in wallet"))?;
|
||||
println!("{}", hex::encode(entry.key_chain.nullifier_public_key.0));
|
||||
println!(
|
||||
"{}",
|
||||
hex::encode(entry.key_chain.viewing_public_key.to_bytes())
|
||||
);
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,8 +156,10 @@ impl WalletSubcommand for GroupSubcommand {
|
||||
let mut z = [0_u8; 32];
|
||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut d);
|
||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut z);
|
||||
let secret = ViewingSecretKey { d, z };
|
||||
let ek_bytes = nssa_core::encryption::ViewingPublicKey::from_seed(&d, &z).0;
|
||||
let secret = ViewingSecretKey::new(d, z);
|
||||
let ek_bytes = nssa_core::encryption::ViewingPublicKey::from_seed(&d, &z)
|
||||
.to_bytes()
|
||||
.to_vec();
|
||||
let public_key = SealingPublicKey::from_bytes(ek_bytes);
|
||||
|
||||
wallet_core.set_sealing_secret_key(secret);
|
||||
|
||||
@ -256,6 +256,31 @@ pub fn read_password_from_stdin() -> Result<String> {
|
||||
Ok(password.trim().to_owned())
|
||||
}
|
||||
|
||||
/// Parse a keys file exported by `wallet account show-keys`.
|
||||
///
|
||||
/// The file format is two lines:
|
||||
/// - Line 1: npk as hex (64 chars, 32 bytes).
|
||||
/// - Line 2: vpk as hex (2368 chars, 1184 bytes).
|
||||
///
|
||||
/// Returns `(npk_bytes, vpk_bytes)`.
|
||||
pub fn read_keys_file(path: &str) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
let content = std::fs::read_to_string(path).with_context(|| {
|
||||
format!("wallet::cli::read_keys_file: failed to read keys file: {path}")
|
||||
})?;
|
||||
let mut lines = content.lines().filter(|l| !l.trim().is_empty());
|
||||
let npk_hex = lines.next().ok_or_else(|| {
|
||||
anyhow::anyhow!("wallet::cli::read_keys_file: keys file is missing npk (line 1)")
|
||||
})?;
|
||||
let vpk_hex = lines.next().ok_or_else(|| {
|
||||
anyhow::anyhow!("wallet::cli::read_keys_file: keys file is missing vpk (line 2)")
|
||||
})?;
|
||||
let npk = hex::decode(npk_hex.trim())
|
||||
.context("wallet::cli::read_keys_file: npk in keys file must be valid hex")?;
|
||||
let vpk = hex::decode(vpk_hex.trim())
|
||||
.context("wallet::cli::read_keys_file: vpk in keys file must be valid hex")?;
|
||||
Ok((npk, vpk))
|
||||
}
|
||||
|
||||
pub fn read_mnemonic_from_stdin() -> Result<Mnemonic> {
|
||||
let mut phrase = String::new();
|
||||
|
||||
@ -299,3 +324,77 @@ pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn read_keys_file_roundtrip() {
|
||||
let npk = [0xab_u8; 32];
|
||||
let vpk = [0xcd_u8; 1184];
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("test.keys");
|
||||
|
||||
// Simulate what `wallet account show-keys` writes.
|
||||
std::fs::write(
|
||||
&path,
|
||||
format!("{}\n{}\n", hex::encode(npk), hex::encode(vpk)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (parsed_npk, parsed_vpk) = read_keys_file(path.to_str().unwrap()).unwrap();
|
||||
|
||||
assert_eq!(parsed_npk, npk, "npk must round-trip through the keys file");
|
||||
assert_eq!(
|
||||
parsed_vpk,
|
||||
vpk.to_vec(),
|
||||
"vpk must round-trip through the keys file"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_keys_file_missing_vpk_returns_error() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("incomplete.keys");
|
||||
std::fs::write(&path, format!("{}\n", hex::encode([0xab_u8; 32]))).unwrap();
|
||||
|
||||
let result = read_keys_file(path.to_str().unwrap());
|
||||
assert!(result.is_err(), "missing vpk line must return an error");
|
||||
assert!(
|
||||
result.unwrap_err().to_string().contains("missing vpk"),
|
||||
"error must mention missing vpk"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_keys_file_invalid_hex_returns_error() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("badhex.keys");
|
||||
std::fs::write(&path, "not-hex\nalso-not-hex\n").unwrap();
|
||||
|
||||
let result = read_keys_file(path.to_str().unwrap());
|
||||
assert!(result.is_err(), "invalid hex must return an error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_keys_file_ignores_blank_lines() {
|
||||
let npk = [0x11_u8; 32];
|
||||
let vpk = [0x22_u8; 1184];
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("blanks.keys");
|
||||
|
||||
// Extra blank lines around the data should be tolerated.
|
||||
std::fs::write(
|
||||
&path,
|
||||
format!("\n{}\n\n{}\n\n", hex::encode(npk), hex::encode(vpk)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (parsed_npk, parsed_vpk) = read_keys_file(path.to_str().unwrap()).unwrap();
|
||||
assert_eq!(parsed_npk, npk);
|
||||
assert_eq!(parsed_vpk, vpk.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Subcommand;
|
||||
use common::transaction::NSSATransaction;
|
||||
use nssa::AccountId;
|
||||
@ -34,13 +34,17 @@ pub enum AuthTransferSubcommand {
|
||||
#[arg(long)]
|
||||
to: Option<CliAccountMention>,
|
||||
/// `to_npk` - valid 32 byte hex string.
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "to_keys")]
|
||||
to_npk: Option<String>,
|
||||
/// `to_vpk` - valid 33 byte hex string.
|
||||
#[arg(long)]
|
||||
/// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes).
|
||||
#[arg(long, conflicts_with = "to_keys")]
|
||||
to_vpk: Option<String>,
|
||||
/// Path to a keys file exported by `wallet account show-keys`, containing npk
|
||||
/// and vpk on separate lines. Replaces `--to-npk` and `--to-vpk`.
|
||||
#[arg(long, conflicts_with_all = ["to_npk", "to_vpk"])]
|
||||
to_keys: Option<String>,
|
||||
/// Identifier for the recipient's private account (only used when sending to a foreign
|
||||
/// private account via `--to-npk`/`--to-vpk`).
|
||||
/// private account via `--to-npk`/`--to-vpk` or `--to-keys`).
|
||||
#[arg(long)]
|
||||
to_identifier: Option<u128>,
|
||||
/// amount - amount of balance to move.
|
||||
@ -100,9 +104,18 @@ impl WalletSubcommand for AuthTransferSubcommand {
|
||||
to,
|
||||
to_npk,
|
||||
to_vpk,
|
||||
to_keys,
|
||||
to_identifier,
|
||||
amount,
|
||||
} => {
|
||||
// Resolve --to-keys into --to-npk / --to-vpk equivalents.
|
||||
let (to_npk, to_vpk) = if let Some(path) = to_keys {
|
||||
let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?;
|
||||
(Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes)))
|
||||
} else {
|
||||
(to_npk, to_vpk)
|
||||
};
|
||||
|
||||
let from = from.resolve(wallet_core.storage())?;
|
||||
let to = to
|
||||
.map(|account_mention| account_mention.resolve(wallet_core.storage()))
|
||||
@ -247,7 +260,7 @@ pub enum NativeTokenTransferProgramSubcommandShielded {
|
||||
/// `to_npk` - valid 32 byte hex string.
|
||||
#[arg(long)]
|
||||
to_npk: String,
|
||||
/// `to_vpk` - valid 33 byte hex string.
|
||||
/// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes).
|
||||
#[arg(long)]
|
||||
to_vpk: String,
|
||||
/// Identifier for the recipient's private account.
|
||||
@ -287,7 +300,7 @@ pub enum NativeTokenTransferProgramSubcommandPrivate {
|
||||
/// `to_npk` - valid 32 byte hex string.
|
||||
#[arg(long)]
|
||||
to_npk: String,
|
||||
/// `to_vpk` - valid 33 byte hex string.
|
||||
/// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes).
|
||||
#[arg(long)]
|
||||
to_vpk: String,
|
||||
/// Identifier for the recipient's private account.
|
||||
@ -339,8 +352,11 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate {
|
||||
to_npk.copy_from_slice(&to_npk_res);
|
||||
let to_npk = nssa_core::NullifierPublicKey(to_npk);
|
||||
|
||||
let to_vpk_res = hex::decode(to_vpk)?;
|
||||
let to_vpk = nssa_core::encryption::ViewingPublicKey(to_vpk_res);
|
||||
let to_vpk_res = hex::decode(&to_vpk)
|
||||
.context("wallet::cli::programs::native_token_transfer: to_vpk must be a valid hex string")?;
|
||||
let to_vpk =
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::from_bytes(to_vpk_res)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let (tx_hash, [secret_from, _]) = NativeTokenTransfer(wallet_core)
|
||||
.send_private_transfer_to_outer_account(
|
||||
@ -413,8 +429,11 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded {
|
||||
to_npk.copy_from_slice(&to_npk_res);
|
||||
let to_npk = nssa_core::NullifierPublicKey(to_npk);
|
||||
|
||||
let to_vpk_res = hex::decode(to_vpk)?;
|
||||
let to_vpk = nssa_core::encryption::ViewingPublicKey(to_vpk_res);
|
||||
let to_vpk_res = hex::decode(&to_vpk)
|
||||
.context("wallet::cli::programs::native_token_transfer: to_vpk must be a valid hex string")?;
|
||||
let to_vpk =
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::from_bytes(to_vpk_res)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let (tx_hash, _) = NativeTokenTransfer(wallet_core)
|
||||
.send_shielded_transfer_to_outer_account(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Subcommand;
|
||||
use common::transaction::NSSATransaction;
|
||||
use nssa::AccountId;
|
||||
@ -41,13 +41,17 @@ pub enum TokenProgramAgnosticSubcommand {
|
||||
#[arg(long)]
|
||||
to: Option<CliAccountMention>,
|
||||
/// `to_npk` - valid 32 byte hex string.
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "to_keys")]
|
||||
to_npk: Option<String>,
|
||||
/// `to_vpk` - valid 33 byte hex string.
|
||||
#[arg(long)]
|
||||
/// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes).
|
||||
#[arg(long, conflicts_with = "to_keys")]
|
||||
to_vpk: Option<String>,
|
||||
/// Path to a keys file exported by `wallet account show-keys`, containing npk
|
||||
/// and vpk on separate lines. Replaces `--to-npk` and `--to-vpk`.
|
||||
#[arg(long, conflicts_with_all = ["to_npk", "to_vpk"])]
|
||||
to_keys: Option<String>,
|
||||
/// Identifier for the recipient's private account (only used when sending to a foreign
|
||||
/// private account via `--to-npk`/`--to-vpk`).
|
||||
/// private account via `--to-npk`/`--to-vpk` or `--to-keys`).
|
||||
#[arg(long)]
|
||||
to_identifier: Option<u128>,
|
||||
/// amount - amount of balance to move.
|
||||
@ -87,13 +91,17 @@ pub enum TokenProgramAgnosticSubcommand {
|
||||
#[arg(long)]
|
||||
holder: Option<CliAccountMention>,
|
||||
/// `holder_npk` - valid 32 byte hex string.
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "holder_keys")]
|
||||
holder_npk: Option<String>,
|
||||
/// `to_vpk` - valid 33 byte hex string.
|
||||
#[arg(long)]
|
||||
/// `holder_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes).
|
||||
#[arg(long, conflicts_with = "holder_keys")]
|
||||
holder_vpk: Option<String>,
|
||||
/// Path to a keys file exported by `wallet account show-keys`, containing npk
|
||||
/// and vpk on separate lines. Replaces `--holder-npk` and `--holder-vpk`.
|
||||
#[arg(long, conflicts_with_all = ["holder_npk", "holder_vpk"])]
|
||||
holder_keys: Option<String>,
|
||||
/// Identifier for the holder's private account (only used when minting to a foreign
|
||||
/// private account via `--holder-npk`/`--holder-vpk`).
|
||||
/// private account via `--holder-npk`/`--holder-vpk` or `--holder-keys`).
|
||||
#[arg(long)]
|
||||
holder_identifier: Option<u128>,
|
||||
/// amount - amount of balance to mint.
|
||||
@ -170,9 +178,17 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
|
||||
to,
|
||||
to_npk,
|
||||
to_vpk,
|
||||
to_keys,
|
||||
to_identifier,
|
||||
amount,
|
||||
} => {
|
||||
let (to_npk, to_vpk) = if let Some(path) = to_keys {
|
||||
let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?;
|
||||
(Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes)))
|
||||
} else {
|
||||
(to_npk, to_vpk)
|
||||
};
|
||||
|
||||
let from = from.resolve(wallet_core.storage())?;
|
||||
let to = to
|
||||
.map(|account_mention| account_mention.resolve(wallet_core.storage()))
|
||||
@ -309,9 +325,17 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
|
||||
holder,
|
||||
holder_npk,
|
||||
holder_vpk,
|
||||
holder_keys,
|
||||
holder_identifier,
|
||||
amount,
|
||||
} => {
|
||||
let (holder_npk, holder_vpk) = if let Some(path) = holder_keys {
|
||||
let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?;
|
||||
(Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes)))
|
||||
} else {
|
||||
(holder_npk, holder_vpk)
|
||||
};
|
||||
|
||||
let definition = definition.resolve(wallet_core.storage())?;
|
||||
let holder = holder
|
||||
.map(|account_mention| account_mention.resolve(wallet_core.storage()))
|
||||
@ -475,7 +499,7 @@ pub enum TokenProgramSubcommandPrivate {
|
||||
/// `recipient_npk` - valid 32 byte hex string.
|
||||
#[arg(long)]
|
||||
recipient_npk: String,
|
||||
/// `recipient_vpk` - valid 33 byte hex string.
|
||||
/// `recipient_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes).
|
||||
#[arg(long)]
|
||||
recipient_vpk: String,
|
||||
/// Identifier for the recipient's private account.
|
||||
@ -569,7 +593,7 @@ pub enum TokenProgramSubcommandShielded {
|
||||
/// `recipient_npk` - valid 32 byte hex string.
|
||||
#[arg(long)]
|
||||
recipient_npk: String,
|
||||
/// `recipient_vpk` - valid 33 byte hex string.
|
||||
/// `recipient_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes).
|
||||
#[arg(long)]
|
||||
recipient_vpk: String,
|
||||
/// Identifier for the recipient's private account.
|
||||
@ -764,8 +788,12 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
|
||||
recipient_npk.copy_from_slice(&recipient_npk_res);
|
||||
let recipient_npk = nssa_core::NullifierPublicKey(recipient_npk);
|
||||
|
||||
let recipient_vpk_res = hex::decode(recipient_vpk)?;
|
||||
let recipient_vpk = nssa_core::encryption::ViewingPublicKey(recipient_vpk_res);
|
||||
let recipient_vpk_res = hex::decode(&recipient_vpk).context(
|
||||
"wallet::cli::programs::token: recipient_vpk must be a valid hex string",
|
||||
)?;
|
||||
let recipient_vpk =
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::from_bytes(recipient_vpk_res)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let (tx_hash, [secret_sender, _]) = Token(wallet_core)
|
||||
.send_transfer_transaction_private_foreign_account(
|
||||
@ -872,8 +900,12 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
|
||||
holder_npk.copy_from_slice(&holder_npk_res);
|
||||
let holder_npk = nssa_core::NullifierPublicKey(holder_npk);
|
||||
|
||||
let holder_vpk_res = hex::decode(holder_vpk)?;
|
||||
let holder_vpk = nssa_core::encryption::ViewingPublicKey(holder_vpk_res);
|
||||
let holder_vpk_res = hex::decode(&holder_vpk).context(
|
||||
"wallet::cli::programs::token: holder_vpk must be a valid hex string",
|
||||
)?;
|
||||
let holder_vpk =
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::from_bytes(holder_vpk_res)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let (tx_hash, [secret_definition, _]) = Token(wallet_core)
|
||||
.send_mint_transaction_private_foreign_account(
|
||||
@ -1024,8 +1056,12 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
|
||||
recipient_npk.copy_from_slice(&recipient_npk_res);
|
||||
let recipient_npk = nssa_core::NullifierPublicKey(recipient_npk);
|
||||
|
||||
let recipient_vpk_res = hex::decode(recipient_vpk)?;
|
||||
let recipient_vpk = nssa_core::encryption::ViewingPublicKey(recipient_vpk_res);
|
||||
let recipient_vpk_res = hex::decode(&recipient_vpk).context(
|
||||
"wallet::cli::programs::token: recipient_vpk must be a valid hex string",
|
||||
)?;
|
||||
let recipient_vpk =
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::from_bytes(recipient_vpk_res)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let (tx_hash, _) = Token(wallet_core)
|
||||
.send_transfer_transaction_shielded_foreign_account(
|
||||
@ -1151,8 +1187,12 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
|
||||
holder_npk.copy_from_slice(&holder_npk_res);
|
||||
let holder_npk = nssa_core::NullifierPublicKey(holder_npk);
|
||||
|
||||
let holder_vpk_res = hex::decode(holder_vpk)?;
|
||||
let holder_vpk = nssa_core::encryption::ViewingPublicKey(holder_vpk_res);
|
||||
let holder_vpk_res = hex::decode(&holder_vpk).context(
|
||||
"wallet::cli::programs::token: holder_vpk must be a valid hex string",
|
||||
)?;
|
||||
let holder_vpk =
|
||||
nssa_core::encryption::MlKem768EncapsulationKey::from_bytes(holder_vpk_res)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let (tx_hash, _) = Token(wallet_core)
|
||||
.send_mint_transaction_shielded_foreign_account(
|
||||
|
||||
@ -684,8 +684,8 @@ impl WalletCore {
|
||||
.filter_map(move |(ciph_id, encrypted_data)| {
|
||||
let ciphertext = &encrypted_data.ciphertext;
|
||||
let commitment = &new_commitments[ciph_id];
|
||||
let shared_secret = key_chain
|
||||
.calculate_shared_secret_receiver(&encrypted_data.epk)?;
|
||||
let shared_secret =
|
||||
key_chain.calculate_shared_secret_receiver(&encrypted_data.epk)?;
|
||||
|
||||
nssa_core::EncryptionScheme::decrypt(
|
||||
ciphertext,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user