diff --git a/crates/core/keys/src/keys.rs b/crates/core/keys/src/keys.rs index e4a48eef5f..dd60929408 100644 --- a/crates/core/keys/src/keys.rs +++ b/crates/core/keys/src/keys.rs @@ -13,6 +13,9 @@ pub use spend::{SpendKey, SpendKeyBytes, SPENDKEY_LEN_BYTES}; mod bip44; pub use bip44::Bip44Path; +mod account_group; +pub use account_group::AccountGroupId; + mod fvk; mod ivk; mod ovk; @@ -20,7 +23,7 @@ mod ovk; pub(crate) use fvk::IVK_DOMAIN_SEP; pub use fvk::{ r1cs::{AuthorizationKeyVar, RandomizedVerificationKey, SpendAuthRandomizerVar}, - AccountGroupId, FullViewingKey, + FullViewingKey, }; pub use ivk::{IncomingViewingKey, IncomingViewingKeyVar, IVK_LEN_BYTES}; pub use ovk::{OutgoingViewingKey, OVK_LEN_BYTES}; diff --git a/crates/core/keys/src/keys/account_group.rs b/crates/core/keys/src/keys/account_group.rs new file mode 100644 index 0000000000..0ec82d87c1 --- /dev/null +++ b/crates/core/keys/src/keys/account_group.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; + +use penumbra_proto::core::keys::v1alpha1; +use penumbra_proto::{penumbra::core::keys::v1alpha1 as pb, serializers::bech32str}; + +/// The hash of a full viewing key, used as an account identifier. +#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[serde(try_from = "pb::AccountGroupId", into = "pb::AccountGroupId")] +pub struct AccountGroupId(pub [u8; 32]); + +impl TryFrom for AccountGroupId { + type Error = anyhow::Error; + + fn try_from(value: v1alpha1::AccountGroupId) -> Result { + Ok(AccountGroupId( + value + .inner + .try_into() + .map_err(|_| anyhow::anyhow!("expected 32 byte array"))?, + )) + } +} + +impl From for v1alpha1::AccountGroupId { + fn from(value: AccountGroupId) -> v1alpha1::AccountGroupId { + v1alpha1::AccountGroupId { + inner: value.0.to_vec(), + } + } +} + +impl std::fmt::Debug for AccountGroupId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + ::fmt(self, f) + } +} + +impl std::fmt::Display for AccountGroupId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str(&bech32str::encode( + &self.0, + bech32str::account_group_id::BECH32_PREFIX, + bech32str::Bech32m, + )) + } +} diff --git a/crates/core/keys/src/keys/fvk.rs b/crates/core/keys/src/keys/fvk.rs index a05b8bad45..8d99d0dfb7 100644 --- a/crates/core/keys/src/keys/fvk.rs +++ b/crates/core/keys/src/keys/fvk.rs @@ -4,22 +4,25 @@ use ark_serialize::CanonicalDeserialize; use decaf377::FieldExt; use decaf377::{Fq, Fr}; use once_cell::sync::Lazy; -use penumbra_proto::{ - penumbra::core::keys::v1alpha1 as pb, serializers::bech32str, DomainType, TypeUrl, -}; use poseidon377::hash_2; use rand_core::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; -pub mod r1cs; +use penumbra_proto::{ + penumbra::core::keys::v1alpha1 as pb, serializers::bech32str, DomainType, TypeUrl, +}; -use super::{AddressIndex, DiversifierKey, IncomingViewingKey, NullifierKey, OutgoingViewingKey}; +use crate::keys::account_group::AccountGroupId; use crate::{ fmd, ka, prf, rdsa::{SpendAuth, VerificationKey}, Address, AddressView, }; +use super::{AddressIndex, DiversifierKey, IncomingViewingKey, NullifierKey, OutgoingViewingKey}; + +pub mod r1cs; + pub(crate) static IVK_DOMAIN_SEP: Lazy = Lazy::new(|| Fq::from_le_bytes_mod_order(b"penumbra.derive.ivk")); @@ -36,11 +39,6 @@ pub struct FullViewingKey { ivk: IncomingViewingKey, } -/// The hash of a full viewing key, used as an account identifier. -#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -#[serde(try_from = "pb::AccountGroupId", into = "pb::AccountGroupId")] -pub struct AccountGroupId(pub [u8; 32]); - impl FullViewingKey { /// Derive a shielded payment address with the given [`AddressIndex`]. pub fn payment_address(&self, index: AddressIndex) -> (Address, fmd::DetectionKey) { @@ -214,39 +212,3 @@ impl std::str::FromStr for FullViewingKey { .try_into() } } - -impl TryFrom for AccountGroupId { - type Error = anyhow::Error; - - fn try_from(value: pb::AccountGroupId) -> Result { - Ok(AccountGroupId( - value - .inner - .try_into() - .map_err(|_| anyhow::anyhow!("expected 32 byte array"))?, - )) - } -} - -impl From for pb::AccountGroupId { - fn from(value: AccountGroupId) -> pb::AccountGroupId { - pb::AccountGroupId { - inner: value.0.to_vec(), - } - } -} - -impl std::fmt::Debug for AccountGroupId { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - // TODO: bech32 - f.debug_tuple("AccountGroupId") - .field(&hex::encode(self.0)) - .finish() - } -} - -impl std::fmt::Display for AccountGroupId { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.write_str(&hex::encode(self.0)) - } -} diff --git a/crates/core/keys/tests/test_account_group.rs b/crates/core/keys/tests/test_account_group.rs new file mode 100644 index 0000000000..0ad2810662 --- /dev/null +++ b/crates/core/keys/tests/test_account_group.rs @@ -0,0 +1,31 @@ +extern crate core; + +use std::str::FromStr; + +use penumbra_keys::keys::{SeedPhrase, SpendKey}; +use penumbra_proto::serializers::bech32str; + +#[test] +fn account_group_id_to_bech32() { + let seed = SeedPhrase::from_str("comfort ten front cycle churn burger oak absent rice ice urge result art couple benefit cabbage frequent obscure hurry trick segment cool job debate").unwrap(); + let spend_key = SpendKey::from_seed_phrase_bip39(seed, 0); + let fvk = spend_key.full_viewing_key(); + let account_group_id = fvk.account_group_id(); + let actual_bech32_str = account_group_id.to_string(); + + let expected_bech32_str = + "penumbraaccountgroupid15r7q7qsf3hhsgj0g530n7ng9acdacmmx9ajknjz38dyt90u9gcgs767wla" + .to_string(); + + assert_eq!(expected_bech32_str, actual_bech32_str); + + // Decoding returns original inner vec + let inner_bytes = bech32str::decode( + &expected_bech32_str, + bech32str::account_group_id::BECH32_PREFIX, + bech32str::Bech32m, + ) + .unwrap(); + + assert_eq!(account_group_id.0, inner_bytes.as_slice()); +} diff --git a/crates/proto/src/serializers/bech32str.rs b/crates/proto/src/serializers/bech32str.rs index c001253ee3..b5cd4c9847 100644 --- a/crates/proto/src/serializers/bech32str.rs +++ b/crates/proto/src/serializers/bech32str.rs @@ -183,6 +183,28 @@ pub mod full_viewing_key { } } +pub mod account_group_id { + use super::*; + + /// The Bech32 prefix used for account group ids. + pub const BECH32_PREFIX: &str = "penumbraaccountgroupid"; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + deserialize_bech32(deserializer, BECH32_PREFIX, Variant::Bech32m) + } + + pub fn serialize(value: &T, serializer: S) -> Result + where + S: Serializer, + T: AsRef<[u8]>, + { + serialize_bech32(value, serializer, BECH32_PREFIX, Variant::Bech32m) + } +} + pub mod spend_key { use super::*; diff --git a/crates/wasm/src/keys.rs b/crates/wasm/src/keys.rs index 15368de722..f1ab1f8077 100644 --- a/crates/wasm/src/keys.rs +++ b/crates/wasm/src/keys.rs @@ -1,10 +1,13 @@ -use crate::error::WasmResult; +use std::str::FromStr; + +use rand_core::OsRng; +use wasm_bindgen::prelude::*; + use penumbra_keys::keys::{SeedPhrase, SpendKey}; use penumbra_keys::{Address, FullViewingKey}; use penumbra_proto::{core::keys::v1alpha1 as pb, serializers::bech32str, DomainType}; -use rand_core::OsRng; -use std::str::FromStr; -use wasm_bindgen::prelude::*; + +use crate::error::WasmResult; /// generate a spend key from a seed phrase /// Arguments: @@ -46,6 +49,16 @@ pub fn get_full_viewing_key(spend_key: &str) -> WasmResult { Ok(JsValue::from_str(&fvk_bech32)) } +/// Account Group Id: the hash of a full viewing key, used as an account identifier +/// Arguments: +/// full_viewing_key: `bech32 string` +/// Returns: `bech32 string` +#[wasm_bindgen] +pub fn get_account_group_id(full_viewing_key: &str) -> WasmResult { + let fvk = FullViewingKey::from_str(full_viewing_key)?; + Ok(fvk.account_group_id().to_string()) +} + /// get address by index using FVK /// Arguments: /// full_viewing_key: `bech32 string` diff --git a/crates/wasm/src/lib.rs b/crates/wasm/src/lib.rs index 8fa1afbd9b..df830d2107 100644 --- a/crates/wasm/src/lib.rs +++ b/crates/wasm/src/lib.rs @@ -1,8 +1,10 @@ #![allow(dead_code)] extern crate core; -mod error; -mod keys; +pub use view_server::ViewServer; + +pub mod error; +pub mod keys; mod note_record; mod planner; mod storage; @@ -11,5 +13,3 @@ mod tx; mod utils; mod view_server; mod wasm_planner; - -pub use view_server::ViewServer; diff --git a/crates/wasm/tests/test_keys.rs b/crates/wasm/tests/test_keys.rs new file mode 100644 index 0000000000..1f6ded728f --- /dev/null +++ b/crates/wasm/tests/test_keys.rs @@ -0,0 +1,20 @@ +extern crate core; + +use penumbra_wasm::keys::get_account_group_id; + +#[test] +fn successfully_get_account_group_id() { + let fvk_str = "penumbrafullviewingkey1sjeaceqzgaeye2ksnz8q73mp6rpx2ykdtzs8wurrnhwdn8vqwuxhxtjdndrjc74udjh0uch0tatnrd93q50wp9pfk86h3lgpew8lsqsz2a6la".to_string(); + let actual_bech32_str = get_account_group_id(&fvk_str).unwrap(); + let expected_bech32_str = + "penumbraaccountgroupid15r7q7qsf3hhsgj0g530n7ng9acdacmmx9ajknjz38dyt90u9gcgs767wla" + .to_string(); + assert_eq!(expected_bech32_str, actual_bech32_str); +} + +#[test] +fn raises_if_fvk_invalid() { + let fvk_str = "invalid".to_string(); + let err = get_account_group_id(&fvk_str).unwrap_err(); + assert_eq!("invalid length", err.to_string()); +}