Viewing Keys #104

Merged
pitmutt merged 101 commits from rav001 into master 2025-01-02 18:36:21 +00:00
9 changed files with 416 additions and 4 deletions

View file

@ -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

View file

@ -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<u8>,
sapling: Vec<u8>,
transparent: Vec<u8>
}
impl<RW> ToHaskell<RW> for Huivk {
fn to_haskell<W: Write>(&self, writer: &mut W, _tag: PhantomData<RW>) -> 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<u8>,
@ -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<u8> = 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<u8> = 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<u8> = 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<u8> = 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);
}
}
}

View file

@ -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'&
}
-> `()'
#}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 <https://git.vergara.tech/Vergara_Tech/zcash-haskell#readme>
category: Blockchain