diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c70e51..048a597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.8.0] + +## Added + +- New `UnifiedIncomingViewingKey` type +- Functions to derive Orchard full viewing key +- Functions to derive Sapling full viewing key +- Functions to derive transparent "full viewing key" +- Functions to encode Unified Full Viewing Keys +- Functions to encode Unified Incoming Viewing Keys + ## [0.7.7.0] ### Changed diff --git a/librustzcash-wrapper/src/lib.rs b/librustzcash-wrapper/src/lib.rs index cb1fbbe..025b49b 100644 --- a/librustzcash-wrapper/src/lib.rs +++ b/librustzcash-wrapper/src/lib.rs @@ -135,7 +135,7 @@ use zcash_primitives::{ use zcash_address::{ Network, - unified::{Address, Encoding, Ufvk, Container, Fvk, Receiver}, + unified::{Address, Encoding, Ufvk, Uivk, Ivk, Container, Fvk, Receiver}, ZcashAddress }; @@ -690,6 +690,35 @@ impl Hufvk { } } +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct Huivk { + net: u8, + orchard: Vec, + sapling: Vec, + transparent: Vec +} + +impl ToHaskell for Huivk { + fn to_haskell(&self, writer: &mut W, _tag: PhantomData) -> Result<()> { + self.serialize(writer)?; + Ok(()) + } +} + +impl Huivk { + fn add_key_section(&mut self, ivk: &Ivk) { + if let Ivk::Orchard(v) = ivk { + self.orchard = v.to_vec(); + } + if let Ivk::Sapling(w) = ivk { + self.sapling = w.to_vec(); + } + if let Ivk::P2pkh(x) = ivk { + self.transparent = x.to_vec(); + } + } +} + #[derive(Debug, BorshSerialize, BorshDeserialize)] pub struct Hsvk { vk: Vec, @@ -968,6 +997,34 @@ pub extern "C" fn rust_wrapper_ufvk_decode( } } +#[no_mangle] +pub extern "C" fn rust_wrapper_uivk_decode( + input: *const u8, + input_len: usize, + out: *mut u8, + out_len: &mut usize + ) { + let input: String = marshall_from_haskell_var(input, input_len, RW); + let dec_key = Uivk::decode(&input); + match dec_key { + Ok((n, uivk)) => { + let x = match n { + Network::Main => 1, + Network::Test => 2, + Network::Regtest => 3 + }; + let mut hk = Huivk { net: x, orchard: vec![0], sapling: vec![0], transparent: vec![0] }; + let ivks = uivk.items(); + ivks.iter().for_each(|k| hk.add_key_section(k)); + marshall_to_haskell_var(&hk, out, out_len, RW); + } + Err(_e) => { + let hk0 = Hufvk { net: 0, orchard: vec![0], sapling: vec![0], transparent: vec![0] }; + marshall_to_haskell_var(&hk0, out, out_len, RW); + } + } +} + #[no_mangle] pub extern "C" fn rust_wrapper_sapling_esk_decrypt( key: *const u8, @@ -2508,3 +2565,87 @@ pub extern "C" fn rust_wrapper_create_transaction( } } } + +#[no_mangle] +pub extern "C" fn rust_wrapper_create_orchard_fvk( + orch_in: *const u8, + orch_in_len: usize, + out: *mut u8, + out_len: &mut usize + ){ + let input: Vec = marshall_from_haskell_var(orch_in, orch_in_len, RW); + let sk = SpendingKey::from_bytes(to_array(input)); + if sk.is_some().into() { + let fvk = FullViewingKey::from(&sk.unwrap()); + let x = Hhex {bytes: fvk.to_bytes().to_vec()}; + marshall_to_haskell_var(&x, out, out_len, RW); + } else { + let x = Hhex {bytes: vec![0]}; + marshall_to_haskell_var(&x, out, out_len, RW); + } +} + +#[no_mangle] +pub extern "C" fn rust_wrapper_create_orchard_ivk( + orch_in: *const u8, + orch_in_len: usize, + out: *mut u8, + out_len: &mut usize + ){ + let input: Vec = marshall_from_haskell_var(orch_in, orch_in_len, RW); + let sk = SpendingKey::from_bytes(to_array(input)); + if sk.is_some().into() { + let fvk = FullViewingKey::from(&sk.unwrap()).to_ivk(Scope::External); + let x = Hhex {bytes: fvk.to_bytes().to_vec()}; + marshall_to_haskell_var(&x, out, out_len, RW); + } else { + let x = Hhex {bytes: vec![0]}; + marshall_to_haskell_var(&x, out, out_len, RW); + } +} + +#[no_mangle] +pub extern "C" fn rust_wrapper_create_sapling_fvk( + sap_in: *const u8, + sap_in_len: usize, + out: *mut u8, + out_len: &mut usize + ){ + let input: Vec = marshall_from_haskell_var(sap_in, sap_in_len, RW); + let in_bytes: [u8; 169] = to_array(input); + let sk = ExtendedSpendingKey::from_bytes(&in_bytes); + match sk { + Ok(k) => { + let fvk = k.to_diversifiable_full_viewing_key(); + let x = Hhex {bytes: fvk.to_bytes().to_vec()}; + marshall_to_haskell_var(&x, out, out_len, RW); + }, + Err(_e) => { + let x = Hhex {bytes: vec![0]}; + marshall_to_haskell_var(&x, out, out_len, RW); + } + } +} + +#[no_mangle] +pub extern "C" fn rust_wrapper_create_sapling_ivk( + sap_in: *const u8, + sap_in_len: usize, + out: *mut u8, + out_len: &mut usize + ){ + let input: Vec = marshall_from_haskell_var(sap_in, sap_in_len, RW); + let in_bytes: [u8; 169] = to_array(input); + let sk = ExtendedSpendingKey::from_bytes(&in_bytes); + match sk { + Ok(k) => { + let ivk = k.to_diversifiable_full_viewing_key().to_external_ivk(); + let x = Hhex {bytes: ivk.to_bytes().to_vec()}; + marshall_to_haskell_var(&x, out, out_len, RW); + }, + Err(_e) => { + let x = Hhex {bytes: vec![0]}; + marshall_to_haskell_var(&x, out, out_len, RW); + } + } +} diff --git a/src/C/Zcash.chs b/src/C/Zcash.chs index fde4550..b4c26de 100644 --- a/src/C/Zcash.chs +++ b/src/C/Zcash.chs @@ -120,6 +120,13 @@ import ZcashHaskell.Types -> `()' #} +{# fun unsafe rust_wrapper_uivk_decode as rustWrapperUivkDecode + { toBorshVar* `BS.ByteString'& + , getVarBuffer `Buffer UnifiedIncomingViewingKey'& + } + -> `()' +#} + {# fun unsafe rust_wrapper_orchard_note_decrypt as rustWrapperOrchardNoteDecode { toBorshVar* `BS.ByteString'& , toBorshVar* `OrchardAction'& @@ -410,3 +417,31 @@ import ZcashHaskell.Types } -> `()' #} + +{# fun unsafe rust_wrapper_create_orchard_fvk as rustWrapperCreateOrchardFvk + { toBorshVar* `BS.ByteString'& + , getVarBuffer `Buffer HexString'& + } + -> `()' +#} + +{# fun unsafe rust_wrapper_create_orchard_ivk as rustWrapperCreateOrchardIvk + { toBorshVar* `BS.ByteString'& + , getVarBuffer `Buffer HexString'& + } + -> `()' +#} + +{# fun unsafe rust_wrapper_create_sapling_fvk as rustWrapperCreateSaplingFvk + { toBorshVar* `BS.ByteString'& + , getVarBuffer `Buffer HexString'& + } + -> `()' +#} + +{# fun unsafe rust_wrapper_create_sapling_ivk as rustWrapperCreateSaplingIvk + { toBorshVar* `BS.ByteString'& + , getVarBuffer `Buffer HexString'& + } + -> `()' +#} diff --git a/src/ZcashHaskell/Keys.hs b/src/ZcashHaskell/Keys.hs index 30484e7..67de27b 100644 --- a/src/ZcashHaskell/Keys.hs +++ b/src/ZcashHaskell/Keys.hs @@ -15,13 +15,42 @@ module ZcashHaskell.Keys where import C.Zcash (rustWrapperGenSeedPhrase, rustWrapperGetSeed) +import Crypto.Secp256k1 (createContext) import qualified Data.ByteString as BS +import Data.HexString (hexBytes) import qualified Data.Text as T +import qualified Data.Text.Encoding as E +import Data.Word (Word8(..)) import Foreign.Rust.Marshall.Variable ( withBorshVarBuffer , withPureBorshVarBuffer ) -import ZcashHaskell.Types (Phrase, Seed(..), ToBytes(..)) +import Haskoin.Address.Base58 (decodeBase58) +import Haskoin.Crypto.Keys.Extended + ( DerivPath(..) + , DerivPathI(..) + , XPubKey(..) + , derivePath + , deriveXPubKey + , xPubExport + ) +import Haskoin.Network.Constants (btc) +import ZcashHaskell.Orchard (deriveOrchardFvk, deriveOrchardIvk) +import ZcashHaskell.Sapling (deriveSaplingFvk, deriveSaplingIvk) +import ZcashHaskell.Types + ( OrchardSpendingKey(..) + , Phrase + , SaplingSpendingKey(..) + , Seed(..) + , ToBytes(..) + , TransparentSpendingKey(..) + , ZcashNet(..) + , uniFullViewingKeyHrp + , uniIncomingViewingKeyHrp + , uniTestFullViewingKeyHrp + , uniTestIncomingViewingKeyHrp + ) +import ZcashHaskell.Utils (encodeBech32m, f4Jumble) -- | Generate a random seed that can be used to generate private keys for shielded addresses and transparent addresses. generateWalletSeedPhrase :: IO Phrase @@ -36,3 +65,97 @@ getWalletSeed p = where result :: Seed result = (withPureBorshVarBuffer . rustWrapperGetSeed) p + +-- | Derive a transparent root node for unified viewing keys +deriveFullTransparentNode :: TransparentSpendingKey -> IO BS.ByteString +deriveFullTransparentNode sk = do + ioCtx <- createContext + let tPubKey = deriveXPubKey ioCtx sk + let tPubKeyBytes = decodeBase58 $ xPubExport btc ioCtx tPubKey + case tPubKeyBytes of + Nothing -> fail "Unable to get transparent key bytes" + Just pb -> return $ BS.takeEnd 65 pb + +-- | Derive a transparent incoming root node for unified incoming viewing keys +deriveIncomingTransparentNode :: TransparentSpendingKey -> IO BS.ByteString +deriveIncomingTransparentNode sk = do + ioCtx <- createContext + let path = Deriv :/ 0 :: DerivPath + let childPrvKey = derivePath ioCtx path sk + let tPubKey = deriveXPubKey ioCtx childPrvKey + let tPubKeyBytes = decodeBase58 $ xPubExport btc ioCtx tPubKey + case tPubKeyBytes of + Nothing -> fail "Unable to get transparent key bytes" + Just pb -> return $ BS.takeEnd 65 pb + +-- | Derive a Unified Full Viewing Key +deriveUfvk :: + ZcashNet + -> OrchardSpendingKey + -> SaplingSpendingKey + -> TransparentSpendingKey + -> IO T.Text +deriveUfvk net okey skey tkey = do + tSec <- deriveFullTransparentNode tkey + let oSec = deriveOrchardFvk okey + let sSec = deriveSaplingFvk skey + case oSec of + Nothing -> fail "Unable to derive Orchard viewing key" + Just oSec' -> do + case sSec of + Nothing -> fail "Unable to derive Sapling viewing key" + Just sSec' -> + return $ encodeVK (hexBytes oSec') (hexBytes sSec') tSec net True + +-- | Derive a Unified Incoming Viewing Key +deriveUivk :: + ZcashNet + -> OrchardSpendingKey + -> SaplingSpendingKey + -> TransparentSpendingKey + -> IO T.Text +deriveUivk net okey skey tkey = do + tSec <- deriveIncomingTransparentNode tkey + let oSec = deriveOrchardIvk okey + let sSec = deriveSaplingIvk skey + case oSec of + Nothing -> fail "Unable to derive Orchard viewing key" + Just oSec' -> do + case sSec of + Nothing -> fail "Unable to derive Sapling viewing key" + Just sSec' -> + return $ encodeVK (hexBytes oSec') (hexBytes sSec') tSec net False + +-- | Encode a Unified Viewing Key per [ZIP-316](https://zips.z.cash/zip-0316) +encodeVK :: + BS.ByteString -- ^ Orchard FVK + -> BS.ByteString -- ^ Sapling FVK + -> BS.ByteString -- ^ Transparent root node + -> ZcashNet -- ^ Network + -> Bool -- ^ Full? + -> T.Text +encodeVK ovk svk tvk net full = encodeBech32m (E.encodeUtf8 hr) b + where + tReceiver = packReceiver 0x00 $ Just tvk + b = f4Jumble $ tReceiver <> sReceiver <> oReceiver <> padding + hr = + if full + then case net of + MainNet -> uniFullViewingKeyHrp + TestNet -> uniTestFullViewingKeyHrp + else case net of + MainNet -> uniIncomingViewingKeyHrp + TestNet -> uniTestIncomingViewingKeyHrp + sReceiver = packReceiver 0x02 $ Just svk + oReceiver = packReceiver 0x03 $ Just ovk + padding = E.encodeUtf8 $ T.justifyLeft 16 '\NUL' hr + packReceiver :: Word8 -> Maybe BS.ByteString -> BS.ByteString + packReceiver typeCode receiver' = + case receiver' of + Just receiver -> + if BS.length receiver > 1 + then BS.singleton typeCode `BS.append` + (BS.singleton . toEnum . BS.length) receiver `BS.append` + receiver + else BS.empty + Nothing -> BS.empty diff --git a/src/ZcashHaskell/Orchard.hs b/src/ZcashHaskell/Orchard.hs index 7991138..b63a914 100644 --- a/src/ZcashHaskell/Orchard.hs +++ b/src/ZcashHaskell/Orchard.hs @@ -19,6 +19,8 @@ module ZcashHaskell.Orchard where import C.Zcash ( rustWrapperCombineOrchardNodes + , rustWrapperCreateOrchardFvk + , rustWrapperCreateOrchardIvk , rustWrapperGenOrchardReceiver , rustWrapperGenOrchardSpendKey , rustWrapperGetOrchardRootTest @@ -37,6 +39,7 @@ import C.Zcash , rustWrapperReadOrchardWitnessAnchor , rustWrapperUADecode , rustWrapperUfvkDecode + , rustWrapperUivkDecode , rustWrapperUpdateOrchardWitness ) import qualified Data.ByteString as BS @@ -157,6 +160,15 @@ decodeUfvk str = where decodedKey = (withPureBorshVarBuffer . rustWrapperUfvkDecode) str +-- | Attempts to decode the given bytestring into a Unified Full Viewing Key +decodeUivk :: BS.ByteString -> Maybe UnifiedIncomingViewingKey +decodeUivk str = + case i_net decodedKey of + 0 -> Nothing + _ -> Just decodedKey + where + decodedKey = (withPureBorshVarBuffer . rustWrapperUivkDecode) str + -- | Check if the given UVK matches the UA given matchOrchardAddress :: BS.ByteString -> BS.ByteString -> Bool matchOrchardAddress = rustWrapperOrchardCheck @@ -337,3 +349,25 @@ compareAddress a u = Sapling s -> s_rec u == Just (sa_receiver s) && ua_net u == net_type s Transparent t -> t_rec u == Just (ta_receiver t) && ua_net u == ta_network t Exchange x -> False + +-- | Derive an Orchard Full Viewing Key +deriveOrchardFvk :: + OrchardSpendingKey -- ^ The Orchard spending key + -> Maybe HexString +deriveOrchardFvk sk = + if BS.length (hexBytes r) > 1 + then Just r + else Nothing + where + r = withPureBorshVarBuffer $ rustWrapperCreateOrchardFvk $ getBytes sk + +-- | Derive an Orchard Incoming Viewing Key +deriveOrchardIvk :: + OrchardSpendingKey -- ^ The Orchard spending key + -> Maybe HexString +deriveOrchardIvk sk = + if BS.length (hexBytes r) > 1 + then Just r + else Nothing + where + r = withPureBorshVarBuffer $ rustWrapperCreateOrchardIvk $ getBytes sk diff --git a/src/ZcashHaskell/Sapling.hs b/src/ZcashHaskell/Sapling.hs index 1347778..7b5cf72 100644 --- a/src/ZcashHaskell/Sapling.hs +++ b/src/ZcashHaskell/Sapling.hs @@ -19,6 +19,8 @@ module ZcashHaskell.Sapling where import C.Zcash ( rustWrapperCombineSaplingNodes + , rustWrapperCreateSaplingFvk + , rustWrapperCreateSaplingIvk , rustWrapperDecodeSaplingAddress , rustWrapperGetSaplingRootTest , rustWrapperIsShielded @@ -318,3 +320,25 @@ decodeSaplingAddress sapling_address = do where sa = withPureBorshVarBuffer $ rustWrapperDecodeSaplingAddress sapling_address + +-- | Derive a Sapling Full Viewing Key +deriveSaplingFvk :: + SaplingSpendingKey -- ^ The Sapling spending key + -> Maybe HexString +deriveSaplingFvk sk = + if BS.length (hexBytes r) > 1 + then Just r + else Nothing + where + r = withPureBorshVarBuffer $ rustWrapperCreateSaplingFvk $ getBytes sk + +-- | Derive a Sapling Incoming Viewing Key +deriveSaplingIvk :: + SaplingSpendingKey -- ^ The Sapling spending key + -> Maybe HexString +deriveSaplingIvk sk = + if BS.length (hexBytes r) > 1 + then Just r + else Nothing + where + r = withPureBorshVarBuffer $ rustWrapperCreateSaplingIvk $ getBytes sk diff --git a/src/ZcashHaskell/Types.hs b/src/ZcashHaskell/Types.hs index a02ed15..d478dc4 100644 --- a/src/ZcashHaskell/Types.hs +++ b/src/ZcashHaskell/Types.hs @@ -696,6 +696,18 @@ data UnifiedFullViewingKey = UnifiedFullViewingKey deriving anyclass (Data.Structured.Show) deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct UnifiedFullViewingKey +-- | Type to represent a Unified Incoming Viewing Key +data UnifiedIncomingViewingKey = UnifiedIncomingViewingKey + { i_net :: !Word8 -- ^ Number representing the network the key belongs to. @1@ for @mainnet@, @2@ for @testnet@ and @3@ for @regtestnet@. + , i_o_key :: !BS.ByteString -- ^ Raw bytes of the Orchard Incoming Viewing Key as specified in [ZIP-316](https://zips.z.cash/zip-0316) + , i_s_key :: !BS.ByteString -- ^ Raw bytes of the Sapling Incoming Viewing Key as specified in [ZIP-316](https://zips.z.cash/zip-0316) + , i_t_key :: !BS.ByteString -- ^ Raw bytes of the P2PKH chain code and public key as specified in [ZIP-316](https://zips.z.cash/zip-0316) + } deriving stock (Eq, Prelude.Show, GHC.Generic) + deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) + deriving anyclass (Data.Structured.Show) + deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct + UnifiedIncomingViewingKey + -- | Type to represent an Orchard Action as provided by the @getrawtransaction@ RPC method of @zcashd@, and defined in the [Zcash Protocol](https://zips.z.cash/protocol/protocol.pdf) data OrchardAction = OrchardAction { nf :: !HexString -- ^ The nullifier of the input note diff --git a/test/Spec.hs b/test/Spec.hs index 9388a8e..207b05a 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -45,7 +45,12 @@ import Test.HUnit import Test.Hspec import Test.Hspec.QuickCheck import Test.QuickCheck -import ZcashHaskell.Keys (generateWalletSeedPhrase, getWalletSeed) +import ZcashHaskell.Keys + ( deriveUfvk + , deriveUivk + , generateWalletSeedPhrase + , getWalletSeed + ) import ZcashHaskell.Orchard import ZcashHaskell.Sapling ( decodeSaplingAddress @@ -1162,6 +1167,33 @@ main = do Just addr -> do let eadr = decodeExchangeAddress (E.encodeUtf8 addr) eadr `shouldNotBe` Nothing + describe "Generate Viewing Keys" $ do + let p = + Phrase + "cloth swing left trap random tornado have great onion element until make shy dad success art tuition canvas thunder apple decade elegant struggle invest" + let seed = getWalletSeed p + let oK = genOrchardSpendingKey (fromJust seed) MainNetCoin 0 + let sK = genSaplingSpendingKey (fromJust seed) MainNetCoin 0 + it "Generate FVK" $ do + tK <- genTransparentPrvKey (fromJust seed) MainNetCoin 0 + case oK of + Nothing -> assertFailure "Failed to generate Orchard SK" + Just o -> + case sK of + Nothing -> assertFailure "Failed to generate Sapling SK" + Just s -> do + fvk <- deriveUfvk MainNet o s tK + decodeUfvk (E.encodeUtf8 fvk) `shouldNotBe` Nothing + it "Generate IVK" $ do + tK <- genTransparentPrvKey (fromJust seed) MainNetCoin 0 + case oK of + Nothing -> assertFailure "Failed to generate Orchard SK" + Just o -> + case sK of + Nothing -> assertFailure "Failed to generate Sapling SK" + Just s -> do + ivk <- deriveUivk MainNet o s tK + decodeUivk (E.encodeUtf8 ivk) `shouldNotBe` Nothing -- | Properties prop_PhraseLength :: Property diff --git a/zcash-haskell.cabal b/zcash-haskell.cabal index 4a2e933..4cbdb81 100644 --- a/zcash-haskell.cabal +++ b/zcash-haskell.cabal @@ -5,7 +5,7 @@ cabal-version: 3.0 -- see: https://github.com/sol/hpack name: zcash-haskell -version: 0.7.7.0 +version: 0.7.8.0 synopsis: Utilities to interact with the Zcash blockchain description: Please see the README on the repo at category: Blockchain