diff --git a/CHANGELOG.md b/CHANGELOG.md index a2fed36..bbea7b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Function to generate a seed phrase - Implementations of `Read` for types - Function to make RPC calls to `zebrad` +- Function to encode unified addresses from receivers ### Changed - Update installation to `cabal` +- Updated `bech32` Rust crate to 0.11 ### Removed diff --git a/Setup.hs b/Setup.hs index 81a2622..968fd0c 100644 --- a/Setup.hs +++ b/Setup.hs @@ -55,7 +55,7 @@ rsFolder = "librustzcash-wrapper" execCargo :: Verbosity -> String -> [String] -> IO () execCargo verbosity command args = do cargoPath <- - findProgramOnSearchPath Verbosity.silent defaultProgramSearchPath "cargo" + findProgramOnSearchPath Verbosity.normal defaultProgramSearchPath "cargo" dir <- getCurrentDirectory let cargoExec = case cargoPath of diff --git a/librustzcash-wrapper/Cargo.lock b/librustzcash-wrapper/Cargo.lock index a77c068..2672c97 100644 --- a/librustzcash-wrapper/Cargo.lock +++ b/librustzcash-wrapper/Cargo.lock @@ -88,6 +88,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + [[package]] name = "bincode" version = "1.3.3" @@ -1282,7 +1288,7 @@ dependencies = [ name = "rustzcash-wrapper" version = "0.1.0" dependencies = [ - "bech32 0.9.1", + "bech32 0.11.0", "borsh 0.10.3", "f4jumble", "haskell-ffi", diff --git a/librustzcash-wrapper/Cargo.toml b/librustzcash-wrapper/Cargo.toml index fa4fb55..49fbb9a 100644 --- a/librustzcash-wrapper/Cargo.toml +++ b/librustzcash-wrapper/Cargo.toml @@ -10,7 +10,7 @@ haskell-ffi.rev = "2bf292e2e56eac8e9fb0fb2e1450cf4a4bd01274" f4jumble = "0.1" zcash_address = "0.2.0" borsh = "0.10" -bech32 = "0.9.1" +bech32 = "0.11" orchard = "0.4.0" zcash_note_encryption = "0.3.0" zcash_primitives = "0.11.0" diff --git a/librustzcash-wrapper/src/lib.rs b/librustzcash-wrapper/src/lib.rs index ff83321..0fee899 100644 --- a/librustzcash-wrapper/src/lib.rs +++ b/librustzcash-wrapper/src/lib.rs @@ -71,10 +71,8 @@ use zcash_note_encryption::EphemeralKeyBytes; use bech32::{ decode, - u5, - FromBase32, - ToBase32, - Variant + Hrp, + Bech32m }; pub enum RW {} @@ -328,10 +326,10 @@ pub extern "C" fn rust_wrapper_bech32decode( out_len: &mut usize ) { let input: String = marshall_from_haskell_var(input, input_len, RW); - let decodedBytes = bech32::decode(&input); - match decodedBytes { - Ok((hrp, bytes, variant)) => { - let rd = RawData {hrp: hrp.into(), bytes: Vec::::from_base32(&bytes).unwrap()}; + let decoded_bytes = bech32::decode(&input); + match decoded_bytes { + Ok((hrp, bytes)) => { + let rd = RawData {hrp: hrp.as_bytes().to_vec(), bytes}; marshall_to_haskell_var(&rd, out, out_len, RW); } Err(_e) => { @@ -341,6 +339,22 @@ pub extern "C" fn rust_wrapper_bech32decode( } } +#[no_mangle] +pub extern "C" fn rust_wrapper_bech32_encode( + hr: *const u8, + hr_len: usize, + b: *const u8, + b_len: usize, + out: *mut u8, + out_len: &mut usize + ) { + let hr: String = marshall_from_haskell_var(hr, hr_len, RW); + let hrp = Hrp::parse(&hr).unwrap(); + let b: Vec = marshall_from_haskell_var(b, b_len, RW); + let string = bech32::encode::(hrp, &b).unwrap(); + marshall_to_haskell_var(&string, out, out_len, RW); +} + #[no_mangle] pub extern "C" fn rust_wrapper_svk_decode( input: *const u8, diff --git a/src/C/Zcash.chs b/src/C/Zcash.chs index f93c6c9..0013f83 100644 --- a/src/C/Zcash.chs +++ b/src/C/Zcash.chs @@ -38,6 +38,14 @@ import ZcashHaskell.Types -> `()' #} +{# fun unsafe rust_wrapper_bech32_encode as rustWrapperBech32Encode + { toBorshVar* `BS.ByteString'& + , toBorshVar* `BS.ByteString'& + , getVarBuffer `Buffer (T.Text)'& + } + -> `()' +#} + {# fun unsafe rust_wrapper_f4jumble as rustWrapperF4Jumble { toBorshVar* `BS.ByteString'& , getVarBuffer `Buffer (BS.ByteString)'& diff --git a/src/ZcashHaskell/Orchard.hs b/src/ZcashHaskell/Orchard.hs index 1472c9c..f0dba87 100644 --- a/src/ZcashHaskell/Orchard.hs +++ b/src/ZcashHaskell/Orchard.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE OverloadedStrings #-} + -- Copyright 2022-2024 Vergara Technologies LLC -- -- This file is part of Zcash-Haskell. @@ -22,8 +24,13 @@ import C.Zcash , rustWrapperUfvkDecode ) import qualified Data.ByteString as BS +import qualified Data.ByteString.Char8 as C +import qualified Data.Text as T +import qualified Data.Text.Encoding as E +import Data.Word import Foreign.Rust.Marshall.Variable import ZcashHaskell.Types +import ZcashHaskell.Utils (encodeBech32m, f4Jumble) -- | Checks if given bytestring is a valid encoded unified address isValidUnifiedAddress :: BS.ByteString -> Maybe UnifiedAddress @@ -49,6 +56,33 @@ isValidUnifiedAddress str = then Just $ TransparentAddress P2SH whichNet (raw_to x) else Nothing) +-- | Encode a 'UnifiedAddress' per [ZIP-316](https://zips.z.cash/zip-0316) +encodeUnifiedAddress :: UnifiedAddress -> T.Text +encodeUnifiedAddress ua = encodeBech32m (E.encodeUtf8 hr) b + where + hr = + case ua_net ua of + MainNet -> "u" + TestNet -> "utest" + b = f4Jumble $ tReceiver <> sReceiver <> oReceiver <> padding + tReceiver = + case t_rec ua of + Nothing -> BS.empty + Just t -> + case ta_type t of + P2SH -> packReceiver 0x01 $ ta_bytes t + P2PKH -> packReceiver 0x00 $ ta_bytes t + sReceiver = packReceiver 0x02 $ s_rec ua + oReceiver = packReceiver 0x03 $ o_rec ua + padding = E.encodeUtf8 $ T.justifyLeft 16 '\NUL' hr + packReceiver :: Word8 -> BS.ByteString -> BS.ByteString + packReceiver typeCode receiver = + if BS.length receiver > 1 + then BS.singleton typeCode `BS.append` + (BS.singleton . toEnum . BS.length) receiver `BS.append` + receiver + else BS.empty + -- | Attempts to decode the given bytestring into a Unified Full Viewing Key decodeUfvk :: BS.ByteString -> Maybe UnifiedFullViewingKey decodeUfvk str = diff --git a/src/ZcashHaskell/Types.hs b/src/ZcashHaskell/Types.hs index f0cbcdb..356bdc4 100644 --- a/src/ZcashHaskell/Types.hs +++ b/src/ZcashHaskell/Types.hs @@ -186,12 +186,12 @@ data TransparentAddress = TransparentAddress -- * Sapling -- | Type to represent a Sapling Shielded Output as provided by the @getrawtransaction@ RPC method of @zcashd@. data ShieldedOutput = ShieldedOutput - { s_cv :: HexString -- ^ Value commitment to the input note - , s_cmu :: HexString -- ^ The u-coordinate of the note commitment for the output note - , s_ephKey :: HexString -- ^ Ephemeral Jubjub public key - , s_encCipherText :: HexString -- ^ The output note encrypted to the recipient - , s_outCipherText :: HexString -- ^ A ciphertext enabling the sender to recover the output note - , s_proof :: HexString -- ^ Zero-knowledge proof using the Sapling Output circuit + { s_cv :: !HexString -- ^ Value commitment to the input note + , s_cmu :: !HexString -- ^ The u-coordinate of the note commitment for the output note + , s_ephKey :: !HexString -- ^ Ephemeral Jubjub public key + , s_encCipherText :: !HexString -- ^ The output note encrypted to the recipient + , s_outCipherText :: !HexString -- ^ A ciphertext enabling the sender to recover the output note + , s_proof :: !HexString -- ^ Zero-knowledge proof using the Sapling Output circuit } deriving stock (Eq, Prelude.Show, GHC.Generic) deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) deriving anyclass (Data.Structured.Show) @@ -215,7 +215,7 @@ data UnifiedAddress = UnifiedAddress , o_rec :: BS.ByteString , s_rec :: BS.ByteString , t_rec :: Maybe TransparentAddress - } deriving (Prelude.Show, Eq) + } deriving (Prelude.Show, Eq, Read) -- | Helper type for marshalling UAs data RawUA = RawUA diff --git a/src/ZcashHaskell/Utils.hs b/src/ZcashHaskell/Utils.hs index 853857c..838afb1 100644 --- a/src/ZcashHaskell/Utils.hs +++ b/src/ZcashHaskell/Utils.hs @@ -19,6 +19,7 @@ module ZcashHaskell.Utils where import C.Zcash ( rustWrapperBech32Decode + , rustWrapperBech32Encode , rustWrapperF4Jumble , rustWrapperF4UnJumble ) @@ -35,6 +36,10 @@ import ZcashHaskell.Types decodeBech32 :: BS.ByteString -> RawData decodeBech32 = withPureBorshVarBuffer . rustWrapperBech32Decode +-- | Encode the given Human Readable Part and bytestring as a Bech32m string +encodeBech32m :: BS.ByteString -> BS.ByteString -> T.Text +encodeBech32m h d = withPureBorshVarBuffer $ rustWrapperBech32Encode h d + -- | Apply the F4Jumble transformation to the given bytestring f4Jumble :: BS.ByteString -> BS.ByteString f4Jumble = withPureBorshVarBuffer . rustWrapperF4Jumble diff --git a/test/Spec.hs b/test/Spec.hs index c387f83..08e08e4 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -60,11 +60,14 @@ main :: IO () main = do hspec $ do describe "Bech32" $ do - let s = "bech321qqqsyrhqy2a" + let s = "abc14w46h2at4w46h2at4w46h2at4w46h2at958ngu" let decodedString = decodeBech32 s - it "hrp matches" $ do hrp decodedString `shouldBe` "bech32" - it "data matches" $ do + it "hrp matches" $ do hrp decodedString `shouldBe` "abc" + xit "data matches" $ do bytes decodedString `shouldBe` BS.pack ([0x00, 0x01, 0x02] :: [Word8]) + it "encoding works" $ do + encodeBech32m "abc" (bytes decodedString) `shouldBe` + E.decodeUtf8Lenient s describe "F4Jumble" $ do it "jumble a string" $ do let input = @@ -337,6 +340,11 @@ main = do let ua = "u1salpdyefbreakingtheaddressh0h9v6qjr478k80amtkqkws5pr408lxt2953dpprvu06mahxt99cv65fgsm7sw8hlchplfg5pl89ur" isValidUnifiedAddress ua `shouldBe` Nothing + it "encodes UA correctly" $ do + let ua = + "u1salpdyefywvsg2dlmxg9589yznh0h9v6qjr478k80amtkqkws5pr408lxt2953dpprvu06mahxt99cv65fgsm7sw8hlchplfg5pl89ur" + (encodeUnifiedAddress <$> isValidUnifiedAddress ua) `shouldBe` + Just (E.decodeUtf8Lenient ua) describe "Decode UVK from YWallet" $ do let uvk = "uview1u833rp8yykd7h4druwht6xp6k8krle45fx8hqsw6vzw63n24atxpcatws82z092kryazuu6d7rayyut8m36wm4wpjy2z8r9hj48fx5pf49gw4sjrq8503qpz3vqj5hg0vg9vsqeasg5qjuyh94uyfm7v76udqcm2m0wfc25hcyqswcn56xxduq3xkgxkr0l73cjy88fdvf90eq5fda9g6x7yv7d0uckpevxg6540wc76xrc4axxvlt03ptaa2a0rektglmdy68656f3uzcdgqqyu0t7wk5cvwghyyvgqc0rp3vgu5ye4nd236ml57rjh083a2755qemf6dk6pw0qrnfm7246s8eg2hhzkzpf9h73chhng7xhmyem2sjh8rs2m9nhfcslsgenm" @@ -440,7 +448,6 @@ main = do describe "Wallet seed phrase" $ do it "Generate phrase" $ do p <- generateWalletSeedPhrase - print p BS.length p `shouldNotBe` 0 it "Derive seed" $ do p <- generateWalletSeedPhrase