Merge pull request #249 from logos-blockchain/marvin/public_keys

public key protocol to match specs
This commit is contained in:
jonesmarvin8 2026-02-11 11:07:32 -05:00 committed by GitHub
commit a0ebd73f48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 214 additions and 84 deletions

1
Cargo.lock generated
View File

@ -3642,6 +3642,7 @@ dependencies = [
"nssa",
"nssa_core",
"rand 0.8.5",
"secp256k1",
"serde",
"sha2",
"thiserror 2.0.17",

View File

@ -10,11 +10,11 @@
"port": 0,
"initial_accounts": [
{
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
"balance": 10000
},
{
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
"balance": 20000
}
],

View File

@ -9,7 +9,7 @@
"initial_accounts": [
{
"Public": {
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
"pub_sign_key": [
16,
162,
@ -48,7 +48,7 @@
},
{
"Public": {
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
"pub_sign_key": [
113,
121,

View File

@ -23,8 +23,8 @@ use wallet::{WalletCore, config::WalletConfigOverrides};
// TODO: Remove this and control time from tests
pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12;
pub const ACC_SENDER: &str = "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy";
pub const ACC_RECEIVER: &str = "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw";
pub const ACC_SENDER: &str = "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV";
pub const ACC_RECEIVER: &str = "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo";
pub const ACC_SENDER_PRIVATE: &str = "3oCG8gqdKLMegw4rRfyaMQvuPHpcASt7xwttsmnZLSkw";
pub const ACC_RECEIVER_PRIVATE: &str = "AKTcXgJ1xoynta1Ec7y6Jso1z1JQtHqd7aPQ1h9er6xX";

View File

@ -5,6 +5,8 @@ edition = "2024"
license = { workspace = true }
[dependencies]
secp256k1 = "0.31.1"
nssa.workspace = true
nssa_core.workspace = true
common.workspace = true
@ -20,4 +22,4 @@ aes-gcm.workspace = true
bip39.workspace = true
hmac-sha512.workspace = true
thiserror.workspace = true
itertools.workspace = true
itertools.workspace = true

View File

@ -1,3 +1,4 @@
use secp256k1::Scalar;
use serde::{Deserialize, Serialize};
use crate::key_management::key_tree::traits::KeyNode;
@ -11,9 +12,32 @@ pub struct ChildKeysPublic {
pub cci: Option<u32>,
}
impl ChildKeysPublic {
fn compute_hash_value(&self, cci: u32) -> [u8; 64] {
let mut hash_input = vec![];
match ((2u32).pow(31)).cmp(&cci) {
// Non-harden
std::cmp::Ordering::Greater => {
hash_input.extend_from_slice(self.cpk.value());
hash_input.extend_from_slice(&cci.to_le_bytes());
hmac_sha512::HMAC::mac(hash_input, self.ccc)
}
// Harden
_ => {
hash_input.extend_from_slice(self.csk.value());
hash_input.extend_from_slice(&(cci).to_le_bytes());
hmac_sha512::HMAC::mac(hash_input, self.ccc)
}
}
}
}
impl KeyNode for ChildKeysPublic {
fn root(seed: [u8; 64]) -> Self {
let hash_value = hmac_sha512::HMAC::mac(seed, "NSSA_master_pub");
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub");
let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap();
let ccc = *hash_value.last_chunk::<32>().unwrap();
@ -28,21 +52,30 @@ impl KeyNode for ChildKeysPublic {
}
fn nth_child(&self, cci: u32) -> Self {
let mut hash_input = vec![];
hash_input.extend_from_slice(self.csk.value());
hash_input.extend_from_slice(&cci.to_le_bytes());
let hash_value = self.compute_hash_value(cci);
let hash_value = hmac_sha512::HMAC::mac(&hash_input, self.ccc);
let csk = nssa::PrivateKey::try_new(
let csk = secp256k1::SecretKey::from_byte_array(
*hash_value
.first_chunk::<32>()
.expect("hash_value is 64 bytes, must be safe to get first 32"),
)
.unwrap();
let csk = nssa::PrivateKey::try_new(
csk.add_tweak(&Scalar::from_le_bytes(*self.csk.value()).unwrap())
.expect("Expect a valid Scalar")
.secret_bytes(),
)
.unwrap();
if secp256k1::constants::CURVE_ORDER < *csk.value() {
panic!("Secret key cannot exceed curve order");
}
let ccc = *hash_value
.last_chunk::<32>()
.expect("hash_value is 64 bytes, must be safe to get last 32");
let cpk = nssa::PublicKey::new_from_private_key(&csk);
Self {
@ -74,59 +107,152 @@ impl<'a> From<&'a ChildKeysPublic> for &'a nssa::PrivateKey {
#[cfg(test)]
mod tests {
use nssa::{PrivateKey, PublicKey};
use super::*;
#[test]
fn test_keys_deterministic_generation() {
let root_keys = ChildKeysPublic::root([42; 64]);
let child_keys = root_keys.nth_child(5);
fn test_master_keys_generation() {
let seed = [
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6,
187, 148, 92, 44, 253, 210, 37,
];
let keys = ChildKeysPublic::root(seed);
assert_eq!(root_keys.cci, None);
assert_eq!(child_keys.cci, Some(5));
let expected_ccc = [
238, 94, 84, 154, 56, 224, 80, 218, 133, 249, 179, 222, 9, 24, 17, 252, 120, 127, 222,
13, 146, 126, 232, 239, 113, 9, 194, 219, 190, 48, 187, 155,
];
assert_eq!(
root_keys.ccc,
[
61, 30, 91, 26, 133, 91, 236, 192, 231, 53, 186, 139, 11, 221, 202, 11, 178, 215,
254, 103, 191, 60, 117, 112, 1, 226, 31, 156, 83, 104, 150, 224
]
);
assert_eq!(
child_keys.ccc,
[
67, 26, 102, 68, 189, 155, 102, 80, 199, 188, 112, 142, 207, 157, 36, 210, 48, 224,
35, 6, 112, 180, 11, 190, 135, 218, 9, 14, 84, 231, 58, 98
]
let expected_csk: PrivateKey = PrivateKey::try_new([
40, 35, 239, 19, 53, 178, 250, 55, 115, 12, 34, 3, 153, 153, 72, 170, 190, 36, 172, 36,
202, 148, 181, 228, 35, 222, 58, 84, 156, 24, 146, 86,
])
.unwrap();
let expected_cpk: PublicKey = PublicKey::try_new([
219, 141, 130, 105, 11, 203, 187, 124, 112, 75, 223, 22, 11, 164, 153, 127, 59, 247,
244, 166, 75, 66, 242, 224, 35, 156, 161, 75, 41, 51, 76, 245,
])
.unwrap();
assert!(expected_ccc == keys.ccc);
assert!(expected_csk == keys.csk);
assert!(expected_cpk == keys.cpk);
}
#[test]
fn test_harden_child_keys_generation() {
let seed = [
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6,
187, 148, 92, 44, 253, 210, 37,
];
let root_keys = ChildKeysPublic::root(seed);
let cci = (2u32).pow(31) + 13;
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
print!(
"{} {}",
child_keys.csk.value()[0],
child_keys.csk.value()[1]
);
assert_eq!(
root_keys.csk.value(),
&[
241, 82, 246, 237, 62, 130, 116, 47, 189, 112, 99, 67, 178, 40, 115, 245, 141, 193,
77, 164, 243, 76, 222, 64, 50, 146, 23, 145, 91, 164, 92, 116
]
);
assert_eq!(
child_keys.csk.value(),
&[
11, 151, 27, 212, 167, 26, 77, 234, 103, 145, 53, 191, 184, 25, 240, 191, 156, 25,
60, 144, 65, 22, 193, 163, 246, 227, 212, 81, 49, 170, 33, 158
]
let expected_ccc = [
126, 175, 244, 41, 41, 173, 134, 103, 139, 140, 195, 86, 194, 147, 116, 48, 71, 107,
253, 235, 114, 139, 60, 115, 226, 205, 215, 248, 240, 190, 196, 6,
];
let expected_csk: PrivateKey = PrivateKey::try_new([
128, 148, 53, 165, 222, 155, 163, 108, 186, 182, 124, 67, 90, 86, 59, 123, 95, 224,
171, 4, 51, 131, 254, 57, 241, 178, 82, 161, 204, 206, 79, 107,
])
.unwrap();
let expected_cpk: PublicKey = PublicKey::try_new([
149, 240, 55, 15, 178, 67, 245, 254, 44, 141, 95, 223, 238, 62, 85, 11, 248, 9, 11, 40,
69, 211, 116, 13, 189, 35, 8, 95, 233, 154, 129, 58,
])
.unwrap();
assert!(expected_ccc == child_keys.ccc);
assert!(expected_csk == child_keys.csk);
assert!(expected_cpk == child_keys.cpk);
}
#[test]
fn test_nonharden_child_keys_generation() {
let seed = [
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6,
187, 148, 92, 44, 253, 210, 37,
];
let root_keys = ChildKeysPublic::root(seed);
let cci = 13;
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
print!(
"{} {}",
child_keys.csk.value()[0],
child_keys.csk.value()[1]
);
assert_eq!(
root_keys.cpk.value(),
&[
220, 170, 95, 177, 121, 37, 86, 166, 56, 238, 232, 72, 21, 106, 107, 217, 158, 74,
133, 91, 143, 244, 155, 15, 2, 230, 223, 169, 13, 20, 163, 138
]
);
assert_eq!(
child_keys.cpk.value(),
&[
152, 249, 236, 111, 132, 96, 184, 122, 21, 179, 240, 15, 234, 155, 164, 144, 108,
110, 120, 74, 176, 147, 196, 168, 243, 186, 203, 79, 97, 17, 194, 52
]
);
let expected_ccc = [
50, 29, 113, 102, 49, 130, 64, 0, 247, 95, 135, 187, 118, 162, 65, 65, 194, 53, 189,
242, 66, 178, 168, 2, 51, 193, 155, 72, 209, 2, 207, 251,
];
let expected_csk: PrivateKey = PrivateKey::try_new([
162, 32, 211, 190, 180, 74, 151, 246, 189, 93, 8, 57, 182, 239, 125, 245, 192, 255, 24,
186, 251, 23, 194, 186, 252, 121, 190, 54, 147, 199, 1, 109,
])
.unwrap();
let expected_cpk: PublicKey = PublicKey::try_new([
183, 48, 207, 170, 221, 111, 118, 9, 40, 67, 123, 162, 159, 169, 34, 157, 23, 37, 232,
102, 231, 187, 199, 191, 205, 146, 159, 22, 79, 100, 10, 223,
])
.unwrap();
assert!(expected_ccc == child_keys.ccc);
assert!(expected_csk == child_keys.csk);
assert!(expected_cpk == child_keys.cpk);
}
#[test]
fn test_edge_case_child_keys_generation_2_power_31() {
let seed = [
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6,
187, 148, 92, 44, 253, 210, 37,
];
let root_keys = ChildKeysPublic::root(seed);
let cci = (2u32).pow(31); //equivant to 0, thus non-harden.
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
let expected_ccc = [
101, 15, 69, 152, 144, 22, 105, 89, 175, 21, 13, 50, 160, 167, 93, 80, 94, 99, 192,
252, 1, 126, 196, 217, 149, 164, 60, 75, 237, 90, 104, 83,
];
let expected_csk: PrivateKey = PrivateKey::try_new([
46, 196, 131, 199, 190, 180, 250, 222, 41, 188, 221, 156, 255, 239, 251, 207, 239, 202,
166, 216, 107, 236, 195, 48, 167, 69, 97, 13, 132, 117, 76, 89,
])
.unwrap();
let expected_cpk: PublicKey = PublicKey::try_new([
93, 151, 154, 238, 175, 198, 53, 146, 255, 43, 37, 52, 214, 165, 69, 161, 38, 20, 68,
166, 143, 80, 149, 216, 124, 203, 240, 114, 168, 111, 33, 83,
])
.unwrap();
assert!(expected_ccc == child_keys.ccc);
assert!(expected_csk == child_keys.csk);
assert!(expected_cpk == child_keys.cpk);
}
}

View File

@ -345,8 +345,8 @@ mod tests {
assert!(tree.key_map.contains_key(&ChainIndex::root()));
assert!(tree.account_id_map.contains_key(&AccountId::new([
46, 223, 229, 177, 59, 18, 189, 219, 153, 31, 249, 90, 112, 230, 180, 164, 80, 25, 106,
159, 14, 238, 1, 192, 91, 8, 210, 165, 199, 41, 60, 104,
172, 82, 222, 249, 164, 16, 148, 184, 219, 56, 92, 145, 203, 220, 251, 89, 214, 178,
38, 30, 108, 202, 251, 241, 148, 200, 125, 185, 93, 227, 189, 247
])));
}

View File

@ -288,12 +288,12 @@ pub mod tests {
let tx = transaction_for_tests();
let expected_signer_account_ids = vec![
AccountId::new([
208, 122, 210, 232, 75, 39, 250, 0, 194, 98, 240, 161, 238, 160, 255, 53, 202, 9,
115, 84, 126, 106, 16, 111, 114, 241, 147, 194, 220, 131, 139, 68,
148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, 112,
83, 153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102,
]),
AccountId::new([
231, 174, 119, 197, 239, 26, 5, 153, 147, 68, 175, 73, 159, 199, 138, 23, 5, 57,
141, 98, 237, 6, 207, 46, 20, 121, 246, 222, 248, 154, 57, 188,
30, 145, 107, 3, 207, 73, 192, 230, 160, 63, 238, 207, 18, 69, 54, 216, 103, 244,
92, 94, 124, 248, 42, 16, 141, 19, 119, 18, 14, 226, 140, 204,
]),
];
let signer_account_ids = tx.signer_account_ids();

View File

@ -48,7 +48,8 @@ impl PublicKey {
impl From<&PublicKey> for AccountId {
fn from(key: &PublicKey) -> Self {
const PUBLIC_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/NSSA/v0.2/AccountId/Public/\x00\x00\x00\x00";
const PUBLIC_ACCOUNT_ID_PREFIX: &[u8; 32] =
b"/LEE/v0.3/AccountId/Public/\x00\x00\x00\x00\x00";
let mut hasher = Sha256::new();
hasher.update(PUBLIC_ACCOUNT_ID_PREFIX);

View File

@ -334,13 +334,13 @@ mod tests {
fn setup_sequencer_config() -> SequencerConfig {
let acc1_account_id: Vec<u8> = vec![
208, 122, 210, 232, 75, 39, 250, 0, 194, 98, 240, 161, 238, 160, 255, 53, 202, 9, 115,
84, 126, 106, 16, 111, 114, 241, 147, 194, 220, 131, 139, 68,
148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, 112, 83,
153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102,
];
let acc2_account_id: Vec<u8> = vec![
231, 174, 119, 197, 239, 26, 5, 153, 147, 68, 175, 73, 159, 199, 138, 23, 5, 57, 141,
98, 237, 6, 207, 46, 20, 121, 246, 222, 248, 154, 57, 188,
30, 145, 107, 3, 207, 73, 192, 230, 160, 63, 238, 207, 18, 69, 54, 216, 103, 244, 92,
94, 124, 248, 42, 16, 141, 19, 119, 18, 14, 226, 140, 204,
];
let initial_acc1 = AccountInitialData {

View File

@ -372,13 +372,13 @@ mod tests {
let tempdir = tempdir().unwrap();
let home = tempdir.path().to_path_buf();
let acc1_id: Vec<u8> = vec![
208, 122, 210, 232, 75, 39, 250, 0, 194, 98, 240, 161, 238, 160, 255, 53, 202, 9, 115,
84, 126, 106, 16, 111, 114, 241, 147, 194, 220, 131, 139, 68,
148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, 112, 83,
153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102,
];
let acc2_id: Vec<u8> = vec![
231, 174, 119, 197, 239, 26, 5, 153, 147, 68, 175, 73, 159, 199, 138, 23, 5, 57, 141,
98, 237, 6, 207, 46, 20, 121, 246, 222, 248, 154, 57, 188,
30, 145, 107, 3, 207, 73, 192, 230, 160, 63, 238, 207, 18, 69, 54, 216, 103, 244, 92,
94, 124, 248, 42, 16, 141, 19, 119, 18, 14, 226, 140, 204,
];
let initial_acc1 = AccountInitialData {
@ -427,8 +427,8 @@ mod tests {
let balance_to_move = 10;
let tx = common::test_utils::create_transaction_native_token_transfer(
[
208, 122, 210, 232, 75, 39, 250, 0, 194, 98, 240, 161, 238, 160, 255, 53, 202, 9,
115, 84, 126, 106, 16, 111, 114, 241, 147, 194, 220, 131, 139, 68,
148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, 112,
83, 153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102,
],
0,
[2; 32],

View File

@ -9,11 +9,11 @@
"port": 3040,
"initial_accounts": [
{
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
"balance": 10000
},
{
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
"balance": 20000
}
],

View File

@ -8,7 +8,7 @@
"initial_accounts": [
{
"Public": {
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
"pub_sign_key": [
16,
162,
@ -47,7 +47,7 @@
},
{
"Public": {
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
"pub_sign_key": [
113,
121,

View File

@ -171,7 +171,7 @@ mod tests {
let initial_acc1 = serde_json::from_str(
r#"{
"Public": {
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
"pub_sign_key": [
16,
162,
@ -214,7 +214,7 @@ mod tests {
let initial_acc2 = serde_json::from_str(
r#"{
"Public": {
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
"pub_sign_key": [
113,
121,

View File

@ -213,7 +213,7 @@ impl Default for WalletConfig {
[
{
"Public": {
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
"pub_sign_key": [
16,
162,
@ -252,7 +252,7 @@ impl Default for WalletConfig {
},
{
"Public": {
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
"pub_sign_key": [
113,
121,