From fef27b09bdf64ccd7e4235fae418c9cb359b5904 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Sat, 19 Aug 2023 07:58:47 -0500 Subject: [PATCH 01/17] Add types for block and raw Tx response --- package.yaml | 1 + src/ZcashHaskell/Types.hs | 100 ++++++++++++++++++++++++++++++++++---- zcash-haskell.cabal | 3 +- 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/package.yaml b/package.yaml index 5c4a606..1db6cc2 100644 --- a/package.yaml +++ b/package.yaml @@ -31,6 +31,7 @@ library: - text - foreign-rust - generics-sop + - aeson pkg-config-dependencies: - rustzcash_wrapper-uninstalled diff --git a/src/ZcashHaskell/Types.hs b/src/ZcashHaskell/Types.hs index 8006e01..8f07073 100644 --- a/src/ZcashHaskell/Types.hs +++ b/src/ZcashHaskell/Types.hs @@ -2,6 +2,7 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingVia #-} {-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE OverloadedStrings #-} -- | -- Module : ZcashHaskell.Types @@ -17,13 +18,16 @@ module ZcashHaskell.Types where import Codec.Borsh +import Data.Aeson import qualified Data.ByteString as BS import Data.Int import Data.Structured +import qualified Data.Text as T import Data.Word import qualified GHC.Generics as GHC import qualified Generics.SOP as SOP +-- * General -- | Type to represent data after Bech32 decoding data RawData = RawData { hrp :: BS.ByteString -- ^ Human-readable part of the Bech32 encoding @@ -33,17 +37,41 @@ data RawData = RawData deriving anyclass (Data.Structured.Show) deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct RawData --- | Type to represent a Unified Full Viewing Key -data UnifiedFullViewingKey = UnifiedFullViewingKey - { net :: Word8 -- ^ Number representing the network the key belongs to. @1@ for @mainnet@, @2@ for @testnet@ and @3@ for @regtestnet@. - , o_key :: BS.ByteString -- ^ Raw bytes of the Orchard Full Viewing Key as specified in [ZIP-316](https://zips.z.cash/zip-0316) - , s_key :: BS.ByteString -- ^ Raw bytes of the Sapling Full Viewing Key as specified in [ZIP-316](https://zips.z.cash/zip-0316) - , 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 UnifiedFullViewingKey +-- * `zcashd` RPC +-- | Type to represent response from the `zcashd` RPC `getblock` method +data BlockResponse = BlockResponse + { bl_confirmations :: Integer -- ^ Block confirmations + , bl_height :: Integer -- ^ Block height + , bl_time :: Integer -- ^ Block time + , bl_txs :: [BS.ByteString] -- ^ List of transaction IDs in the block + } deriving (Prelude.Show, Eq) +instance FromJSON BlockResponse where + parseJSON = + withObject "BlockResponse" $ \obj -> do + c <- obj .: "confirmations" + h <- obj .: "height" + t <- obj .: "time" + txs <- obj .: "tx" + pure $ BlockResponse c h t (BS.pack <$> txs) + +-- | Type to represent response from the `zcashd` RPC `getrawtransaction` +data RawTxResponse = RawTxResponse + { rt_id :: T.Text + , rt_shieldedOutputs :: [ShieldedOutput] + , rt_orchardActions :: [OrchardAction] + } deriving (Prelude.Show, Eq) + +instance FromJSON RawTxResponse where + parseJSON = + withObject "RawTxResponse" $ \obj -> do + i <- obj .: "txid" + s <- obj .: "vShieldedOutput" + o <- obj .: "orchard" + a <- o .: "actions" + pure $ RawTxResponse i s a + +-- * Sapling -- | Type to represent a Sapling Shielded Output as provided by the @getrawtransaction@ RPC method of @zcashd@. data ShieldedOutput = ShieldedOutput { s_cv :: BS.ByteString -- ^ Value commitment to the input note @@ -57,6 +85,36 @@ data ShieldedOutput = ShieldedOutput deriving anyclass (Data.Structured.Show) deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct ShieldedOutput +instance FromJSON ShieldedOutput where + parseJSON = + withObject "ShieldedOutput" $ \obj -> do + cv <- obj .: "cv" + cmu <- obj .: "cmu" + ephKey <- obj .: "ephemeralKey" + encText <- obj .: "encCiphertext" + outText <- obj .: "outCiphertext" + p <- obj .: "proof" + pure $ + ShieldedOutput + (BS.pack cv) + (BS.pack cmu) + (BS.pack ephKey) + (BS.pack encText) + (BS.pack outText) + (BS.pack p) + +-- * Orchard +-- | Type to represent a Unified Full Viewing Key +data UnifiedFullViewingKey = UnifiedFullViewingKey + { net :: Word8 -- ^ Number representing the network the key belongs to. @1@ for @mainnet@, @2@ for @testnet@ and @3@ for @regtestnet@. + , o_key :: BS.ByteString -- ^ Raw bytes of the Orchard Full Viewing Key as specified in [ZIP-316](https://zips.z.cash/zip-0316) + , s_key :: BS.ByteString -- ^ Raw bytes of the Sapling Full Viewing Key as specified in [ZIP-316](https://zips.z.cash/zip-0316) + , 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 UnifiedFullViewingKey + -- | 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 :: BS.ByteString -- ^ The nullifier of the input note @@ -72,6 +130,28 @@ data OrchardAction = OrchardAction deriving anyclass (Data.Structured.Show) deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct OrchardAction +instance FromJSON OrchardAction where + parseJSON = + withObject "OrchardAction" $ \obj -> do + n <- obj .: "nullifier" + r <- obj .: "rk" + c <- obj .: "cmx" + ephKey <- obj .: "ephemeralKey" + encText <- obj .: "encCiphertext" + outText <- obj .: "outCipherText" + cval <- obj .: "cv" + a <- obj .: "spendAuthSig" + pure $ + OrchardAction + (BS.pack n) + (BS.pack r) + (BS.pack c) + (BS.pack ephKey) + (BS.pack encText) + (BS.pack outText) + (BS.pack cval) + (BS.pack a) + -- | Type to represent a decoded Orchard Action data OrchardDecodedAction = OrchardDecodedAction { a_value :: Int64 -- ^ The amount of the transaction in _zatoshis_. diff --git a/zcash-haskell.cabal b/zcash-haskell.cabal index 821d867..0b27eed 100644 --- a/zcash-haskell.cabal +++ b/zcash-haskell.cabal @@ -38,7 +38,8 @@ library pkgconfig-depends: rustzcash_wrapper-uninstalled build-depends: - base >=4.7 && <5 + aeson + , base >=4.7 && <5 , borsh >=0.2 , bytestring , foreign-rust -- 2.34.1 From deb3ef33da35966e99705b439f8ec88f7dcc339d Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Mon, 21 Aug 2023 09:57:45 -0500 Subject: [PATCH 02/17] Add tests for JSON parsers --- CHANGELOG.md | 4 +++ block.json | 75 +++++++++++++++++++++++++++++++++++++++ package.yaml | 1 + src/ZcashHaskell/Types.hs | 35 +++++++++--------- test/Spec.hs | 24 ++++++++++++- tx.json | 70 ++++++++++++++++++++++++++++++++++++ zcash-haskell.cabal | 3 +- 7 files changed, 193 insertions(+), 19 deletions(-) create mode 100644 block.json create mode 100644 tx.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ed02bc6..f339b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Type for block response +- Type for raw transaction response +- JSON parsers for block response, transaction response, `ShieldedOutput` and `OrchardAction` +- Tests for JSON parsers - Haddock annotations ### Changed diff --git a/block.json b/block.json new file mode 100644 index 0000000..d00f0c9 --- /dev/null +++ b/block.json @@ -0,0 +1,75 @@ +{ + "hash": "000000000079250b2cb5f3a04f47623db0f2552abeeb5fef914d8833c827ff63", + "confirmations": 5, + "size": 19301, + "height": 2196277, + "version": 4, + "merkleroot": "bbeb085e2e69afd760e48512f2cc4af788331a19ad03cf1442dc2c38bf1819ef", + "blockcommitments": "9af507deaee501f8a9a9efb367d199b21d08874393f0408412c408352f967845", + "authdataroot": "562acdacbf061ef8ef5b84917247669b45935f83280adfedcd0f9b39efaf25ef", + "finalsaplingroot": "625ebbfa357830e0ecf7b14b149939e9c95c75ef19ae17b32f660783add33196", + "finalorchardroot": "d54d40365258b350642ede76ec8d411220b93b4bd16c63bff803715b87154e0b", + "chainhistoryroot": "b4438f23544049ed0185baca65cfbc06a09eee7577b4fe567e3f6bb08f107c56", + "tx": [ + "795fabb4070cc221480e3b8deba2f76a9c5d16026a5f8e2c29c833e5b6088eb4", + "66637dc7703bbacc385ef7f2e087bd5fcc56763515217822906e352f504eb820", + "b2384cd27fb12cb119754f91077453ffdc553da3be384d156b1f16ce4e88a9c5", + "c4c1c3d962f2e56b65585be3b5a09c7b42e1a6ea66c0f6492ad3d3ea2e0775d0", + "e1acb17e24b7d2df5a2c23349a1fc66d1084b1a9a85cfe760ed72fb37f960a12", + "e5aeac0d023259551616cdec6727219048535aa619bba4e722e887424cf9ebef" + ], + "time": 1692399702, + "nonce": "ddca0340000000000000000000370000000000000000000000000000093e790d", + "solution": "003b65d98e5199710d69e661f6def0120bc519c7bd1a4ec4b727edf953746a261046760f1dd584f743781478251d65a4b7e1f775c192c8f01aecf2301753bd1eb472ea4b9bb33d9c6236d6f94551c6ee699a20be02342d54196ed2a1ce43c0a56cb20baeda8578498a2cd783b49970a65b8bc2c9d45d7b6863b86e5fb5291b5af986da9e11f5342477173b68cd8e58099791b028031725459bb81353f398baee5acb0390243e36e1039720df4108697dab0772b844ded785119a3cb4f30483221042c965efcb0190dbcbe8eac0f4c0ac51a404ec0f06bf83cfae33a9163c73e7402e07c1f59fb01b692167359a5ea2fd30452b723443454e22ec32de0556e899860cb029439e04642f2cc4815265b521e207ba7d794d498157d1f0e364762f32b32a375e483c19f4a7419846fc75be75729a2cff99f8f5b690d58d40a3bd1043a2caeb79aa44a97d792b0d60d1d6c2460105c304c9418fd5f859b1ebb649854a9473394057103edad7e518bf7afe1165ceff7e50365c7b1dac6c3b9e35ea842ce251b041566c3f576e961485770806459a1e752ee2fac542693999ad7c268aecd87d37550285a6a1420ba2af5007c2ac3c678401c92dbb63a423f003537bd7b93961c32314667dc8dddfc49b84dc0896bb7611da7d5347b1019f7aacf3e19c16ddf91d30ddec8f40ea919156aa75b8644981ae909f98f433012173489f443a11e1d9e50649a95299d0aa91b9d50343c70b4c209ce77222a2200dc1406d98bfacc9ac09f98d1e1d440af18b3c8327d0a1c0027e9c7fadfe181a4d62b9d3869d38c542e1b22c271b6f491f49ea0b684b4a3ca841c1ebb5b1efe443cd1b94653cc8d70c220dc95e9611c561f19188391fe2be3b9bf84e2615ca99f87a4d7421964002018b4199c8a9037b44304133c7c4bcd6a55d7aeec4f5d12d9359dbc97802350072885f8f2ea93feaa3e3b03e7afc2ad581b6aea30cafb2ea8891cc0df673b2b8ca5e1a692d3ab32b31132b3e6882937443e872c34818305f390500bb37a921b1094e05d894c6e62913c402bc6deef5989f98990256b0f99c212bd3d810f1459a30f281196edebf531392d72368df449b3ee2a2c3c8a36349bd985215630701decafe90648edebec3f263bd70969955bb839b37a724a9c9d0420abc80e8172fc1ca5a7d3b587ea305fd1d2c021e760cf662f19079bbe56a454e9e284e465adebb3c12d4d9353fa5c002c037af529f3fb9ab067ebf1a7b30807b89803751665f6b5aeea117f03e15d66e1b1aef675b9674d512b5d0d895ca5cd5cc920f35020eaaa76637c198124c2dc33da4d71bdfc49e15f5c79ca4b33f0df22682d5541f2714cba71207d91acecb0fe88dd960eb61a3c8aec32b822b4abc11ba1f63b920191a62b4e4bc42b2b151ed1e701cbd408100bb2b4fe393da9b81b708f3884cee7e7414944a481b1e1c5f2851477acc7803e622ffab7e444d7e8faa3c46d6187ed31d02f3c6790453e67f7ac622db35ac5edee7b72aa4acf16f6bd8cb3dd878c7b0223ef2ce017dcf919d120dc0c83d5401bf4c6baaed245eabea031b3c2fbce6d7a3bd3ea0886e1e0c8067bd724de003c837947284569e5a39666bb7ce0a21af3d11f82114b75d5556504d31e229b3c2942a28f51b378bdb15059e0073e9a60f515770315c0d8dd58ab3b89bd6cd3e9bd2109b67cfe5732ff68cdb6aa0f29b90f92f3707cbed01a0c20bec9c427735af54983ec4369a253521d4c42e4ca1bff59adb02878cd8b26cb952b71a0506305b8ffe695581eae625d23bb4e3be2e84bed7ac193d0267386846efa7ccd1b3b6bd04d52271bf62dd08590125c49f9fefe32a859380bc638fd4f31eecc11087e627b44a7a73786b23614b6864bec39afacf18", + "bits": "1c01b44d", + "difficulty": 78752260.61608158, + "chainwork": "0000000000000000000000000000000000000000000000000e4f2c44f6a82cfb", + "anchor": "638a7385e9910d3e18ae4240735ed4a5f6b0f410b0a1bef9d831452e0cff0a3c", + "chainSupply": { + "monitored": false, + "valueDelta": 3.12500000, + "valueDeltaZat": 312500000 + }, + "valuePools": [ + { + "id": "transparent", + "monitored": false, + "valueDelta": -134.79807867, + "valueDeltaZat": -13479807867 + }, + { + "id": "sprout", + "monitored": true, + "chainValue": 26762.63007004, + "chainValueZat": 2676263007004, + "valueDelta": 0.00000000, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "monitored": true, + "chainValue": 1155712.35104510, + "chainValueZat": 115571235104510, + "valueDelta": 68.96131433, + "valueDeltaZat": 6896131433 + }, + { + "id": "orchard", + "monitored": true, + "chainValue": 96151.73011093, + "chainValueZat": 9615173011093, + "valueDelta": 68.96176434, + "valueDeltaZat": 6896176434 + } + ], + "trees": { + "sapling": { + "size": 72943241 + }, + "orchard": { + "size": 48645942 + } + }, + "previousblockhash": "0000000000a67420fd68bf269b63d821b158cd1da20d067e219adaa66977970d", + "nextblockhash": "00000000016ebe0a0da97446c677478aa30df66b1b503fd297ad895ee7941d5e" +} diff --git a/package.yaml b/package.yaml index 1db6cc2..5f7495e 100644 --- a/package.yaml +++ b/package.yaml @@ -48,3 +48,4 @@ tests: - hspec - bytestring - text + - aeson diff --git a/src/ZcashHaskell/Types.hs b/src/ZcashHaskell/Types.hs index 8f07073..03d7d13 100644 --- a/src/ZcashHaskell/Types.hs +++ b/src/ZcashHaskell/Types.hs @@ -20,6 +20,7 @@ module ZcashHaskell.Types where import Codec.Borsh import Data.Aeson import qualified Data.ByteString as BS +import qualified Data.ByteString.Char8 as C import Data.Int import Data.Structured import qualified Data.Text as T @@ -43,7 +44,7 @@ data BlockResponse = BlockResponse { bl_confirmations :: Integer -- ^ Block confirmations , bl_height :: Integer -- ^ Block height , bl_time :: Integer -- ^ Block time - , bl_txs :: [BS.ByteString] -- ^ List of transaction IDs in the block + , bl_txs :: [T.Text] -- ^ List of transaction IDs in the block } deriving (Prelude.Show, Eq) instance FromJSON BlockResponse where @@ -53,7 +54,7 @@ instance FromJSON BlockResponse where h <- obj .: "height" t <- obj .: "time" txs <- obj .: "tx" - pure $ BlockResponse c h t (BS.pack <$> txs) + pure $ BlockResponse c h t txs -- | Type to represent response from the `zcashd` RPC `getrawtransaction` data RawTxResponse = RawTxResponse @@ -96,12 +97,12 @@ instance FromJSON ShieldedOutput where p <- obj .: "proof" pure $ ShieldedOutput - (BS.pack cv) - (BS.pack cmu) - (BS.pack ephKey) - (BS.pack encText) - (BS.pack outText) - (BS.pack p) + (C.pack cv) + (C.pack cmu) + (C.pack ephKey) + (C.pack encText) + (C.pack outText) + (C.pack p) -- * Orchard -- | Type to represent a Unified Full Viewing Key @@ -138,19 +139,19 @@ instance FromJSON OrchardAction where c <- obj .: "cmx" ephKey <- obj .: "ephemeralKey" encText <- obj .: "encCiphertext" - outText <- obj .: "outCipherText" + outText <- obj .: "outCiphertext" cval <- obj .: "cv" a <- obj .: "spendAuthSig" pure $ OrchardAction - (BS.pack n) - (BS.pack r) - (BS.pack c) - (BS.pack ephKey) - (BS.pack encText) - (BS.pack outText) - (BS.pack cval) - (BS.pack a) + (C.pack n) + (C.pack r) + (C.pack c) + (C.pack ephKey) + (C.pack encText) + (C.pack outText) + (C.pack cval) + (C.pack a) -- | Type to represent a decoded Orchard Action data OrchardDecodedAction = OrchardDecodedAction diff --git a/test/Spec.hs b/test/Spec.hs index 0ad9d47..36def01 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -1,8 +1,12 @@ {-# LANGUAGE OverloadedStrings #-} import C.Zcash (rustWrapperIsUA) +import Data.Aeson import qualified Data.ByteString as BS +import qualified Data.Text as T import qualified Data.Text.Encoding as E +import qualified Data.Text.Lazy.Encoding as LE +import qualified Data.Text.Lazy.IO as LTIO import Data.Word import Test.Hspec import ZcashHaskell.Orchard @@ -12,9 +16,12 @@ import ZcashHaskell.Sapling , matchSaplingAddress ) import ZcashHaskell.Types - ( OrchardAction(..) + ( BlockResponse(..) + , OrchardAction(..) , OrchardDecodedAction(..) , RawData(..) + , RawTxResponse(..) + , ShieldedOutput(s_cmu) , UnifiedFullViewingKey(..) ) import ZcashHaskell.Utils @@ -233,6 +240,21 @@ main = do , 0x22 ] :: [Word8] f4UnJumble (BS.pack out) `shouldBe` BS.pack input + describe "JSON parsing" $ do + it "block response" $ do + j <- LTIO.readFile "block.json" + let p = eitherDecode $ LE.encodeUtf8 j :: Either String BlockResponse + case p of + Left s -> s `shouldBe` "" + Right x -> bl_height x `shouldBe` 2196277 + it "raw transaction response" $ do + j <- LTIO.readFile "tx.json" + let t = eitherDecode $ LE.encodeUtf8 j :: Either String RawTxResponse + case t of + Left s -> s `shouldBe` "" + Right x -> + rt_id x `shouldBe` + "5242b51f22a7d6fe9dee237137271cde704d306a5fff6a862bffaebb6f0e7e56" describe "Sapling address" $ do it "succeeds with valid address" $ do let sa = diff --git a/tx.json b/tx.json new file mode 100644 index 0000000..b66ec68 --- /dev/null +++ b/tx.json @@ -0,0 +1,70 @@ +{ + "hex": "050000800a27a726b4d0d6c200000000ff8e210000000001146cc65bd6d252d09b8eb0a8ab0aab6d7a798325aefc1d3032fc6d31373a85a25a3a16b447a698f720ade1bc290a74d85574b5b20515391035a67f8d5883dc65ea3ba4a17b009d6f325d41072b3ce240270959a7ffd040e5f16c697d8148973c62ffe037fc83aded21e4c91722b52520a2395c23e9c1a896f4b0f12d32ae8e31833d9d95adae40f6ecf7aff52af184efd390a4c1aa76b5fb1cab6003b1a8a004016f385926661f56d38273ec2c3df7775210310a65fff5fa9ac5509f0784eefea28bdcc67b0ff69eef930335f3b9768529e2bfe733024492101f642f989de8cbf04dd66638e9317780bce47085079675b772664c8007e96597dba83ea9af22ddf07ff1c212983d4a902914431245357527294e69ea5616e720ef1e9215bbfa33ba108b8d07efff2bad1850525d7725c681761c9b8c844a5548afabf176863de7b4cde3901defc3e83d31086d3c6e6af9a5fcc3cfb38b52ac7de84f91df5e0587f7603773401a62eeef10cd3ccf4d927ef42402c32f32280abbeaac33e73ceda52089820a186e9a1adfea81453998c6bbaa0deb41bc4f94586bfee80bad25fc71abe7c6dd44bcb1a6929a0112c7e4f8fcadb9745bde9422b954f72954c4d22db48719de61f383d620935b647337f73d119d79fd208e1d5a92f0855447df5782cd4764ba91efa65d9e4ebaa34e2eccb7aac93a5b0efe0c7664f3cd9384b3ff706ad3019c907cdcfa084351c9f6a0bfa8c78c91272ca66ac86dd6e1d0d6ba9704ea7dc54f71a053dce91f844c1ca62b5ddfe6b53834f4a816b1b01460810d9b87517659f4915adf4b84783a60ecf3bd71569259f1ff90a91a0b314bd4c77976d7893bf42e5d6ad0f8df95eb6d6c69d41490be8e39b2452df3bebfc297d5b0fc97f081890390fb0727a96898585f0120a7da9a798f2032590553f724d8756c67c5b0d1c0d23301c4ed60fa283994fd712aab17ca6360256fd5aef0ebc48f0256e3eda5894b53981d0d46768aefdc85b48c1525b7f134dce5d4ec2d76c03c821513f1652d9671219d744bdce5e69b9a74ca0c7c837668f0d8ffffffffffff9534b3d594e1609b3bace18608750b35a066c57f85e291d194400cb351430bbbe212abba32be071e747b7310863bd5fd989855a6567a351b288144b6e9f838c6a517db94673246ef0010b65f9c0be8aca654f6f57b83d893663cfd389ab96ce50e8077fe588c16b1b5989c6cc262e6658efb9b88ac800e49e9e5999e2651b8fff28fa77071d63790df155ed8344e2581ac5205b31d4f17bd748fcf60e35a9d6048d23c94c7aca8d4e541fda497aa268df9c173af5877a5da56d8fa2a42166900c734b62e56792f6c8bed48e4f108a817e83d64d6a59e38cfdb55c0f8a89bc7507c89326266f7ac03a3941f448cb879bd792bb116d0be8876c0856a76ddec0f0c02e16f0338626013ee5f6037fc6a3c69fa291204039d04d17c11295ee3024aea8f5d381e9b7eb3f938b6f9182bf4f889f1e53e30f998b1cdd23f45cfaa20aaef058248cc2e1c487fcdf54a4bc22a68a17cb6fa7b2fbf333b99feb84643d321398b675634929602126b2fb40171e514769bf82f18c267ce9cda0c24300caa9a5a361144d3b7b9ab2243ee9811d9b2e72c8bb1d145cdfcf6b29994a969b41c47208f5dba8d6d871e490e9b970afec4d8bca40ba51825cdc78cc7cde6b6f235a4105b1d1b5e2765efd753095ce770f070b02cce3316721b9345680c146c2f428c0bbca90d5a8cd0a1c4c31cbfa8ec165ea9f9c71d2d05e3cf8bae5e779786f179c45a3cd8087d820cae812aded04f8acda9068af80ea834f79f1bd03bfd66f8a19074649a85ce877df1a621a867debb423ec0d19015b326fcf6f143aba34029c1da2fc7b099378a366c38c9609ef6a9d9e175e21b0c1ab94a84e28ee7f1a00e39cb6fb59f44e4567e9f85f8f98158263c52ec433c042397c784edb07c28d2bca036f59090e819157375d610acb1993a4107b48da13a371f5383429baee209b2c0cc150fcef79a042749668ba1f89ad24a8c746142191ed0e8fd63624a331d98d50daa84ccf9043076947cf5115b9f8787acd36000c5e72c8d783b29bb28a3e46036d0a592ce8a158ee5a7ac210be72d3a6185c13645d96a8446021b64043ab8b589a20091c152e7d5a993ba94770eea988e289e1536d0d81dbc7046ca9c6d918446bf970894f073c920006681ccf6d1a3f138519c68eba0296069e42dc60f2bcd0f17c400efe4f4e87de8606606dc4fdf31494df4d454d14a440b1d9db4265c7aa9bc8683c68cb149f2cc826427575e2af82e842199a9cb9fdc7243b3bc12f1a71c37eac5cf88ba830cb95728897fa4c177a290d6b2b3814173262da14db9b4ef39fc54f888a6ffef4221ae672fb03bc78ebef479360a682ddb12ea0369a428a6c2960ff8327e9a2f5e5d98ce1eae748db8f6a4631c789b4d751d6b99c97c149a813998d44a7b57ba06c8bcb8a6c73c6388cdcfeb1346cec8fee7bdebf2a2388d9722183eb2d2e0e183cdd092152ef640880f4514f3c5e836cc3a8249413500630aa8da85f9e3cd92bdadbb69a2bab8d71f0b3ec5832a7ddbddd67b34c33b2e12a0c8468e852e4a8f7df45657e9632088aa7c6c5048a2686019cfec33b27fc88e23759938dd55a5dff589c1c21a37da617609e9d8be37dbf9bd6e84ee160fe10268171d969e4611afe9d3482ed4b132dcdd11ee516f36d512a333da20266fd984caebf4937fdfd18ed07b4a45771cf5c8c16c6b258b289a07d136a22acc766011f366c420bafb8fc1a10e42219bede5a3d1166c525491ab60bbd1f973fd3fb2e94cea888e24d5fb0adce51faeda75d62de70094d4b36d38d03cd824d284fad577c3ead4d98bcc8ceccd18174a889b22380bfcc12656e764ea0b8fe1409971283008ed02cbef89d6f544c62c3b001bfe96723fda9190deecba534d69cfa358036fdaf16127b89f925c52d4e750919ffb7182b6a8ad13d0a8e00e0b906978dd24ee11869c1a63837a80e46e1216e2e273aba07aa5b0d97558db0ba7f9ac4c89403c65f1719394e479311f5cf84746e6be6f1abcac03194aa8bf1735811198b5df90dd6cac345779c185c24beda0101b932048dc4144af664a63acc0c395052882ee1f18bd0ddf13bb583861923bc00ed5ae815b964698ca097eda1c4281e039139fa3091890244f926cc4ab773ca8a35d5263d3bb48fd6ac53a4bb4d7d60b36446dbc714c35b5e13a17c5b0c70f67207839d1f7404604aff63b2fa83a4da7dac92aac96b3f250412f8d04a9e298004313b02edefd076c67d8a1316355777814e2e1ab03690e426b672d32ff65c03c592ecce6a70e34fea2e15b9a6b4fd092d027199caf27e84e25c09380b38a5eb8985355b3259aa1d94be74269b84f953053b02ba3be9df872ae5fb2d893188575bdfe222ba267b5461a0d0be274a7d9e6ee51490d98e4cd97978804c4f0f8e9f4908fd8c102b01080f5a02b7578591e95d60f3f56d8e48514b1ce7ea6894f55a32c8ac8564985d18c6b82f8dcde5b315624e9321bdd49dd350c87907cc373c0238a79321e6250e38a0ceb2c060ecee6708c11cb30a49687da9923bcdf011f9aca27e6eb5a8477a2bae2dcff9884cc2349b51a66b5179ed2d8f69e4bbba74c694194e83d04a8566228227eb732a95180c6788483d1f259d52c52fe43357656d50a1cf2902c3124d60d15fc85f0447a1203f824c1106452cfec1c92b18de003f82a0000000000001cbd27436a221a53d08c4838831d1bc60ff7e93df41a51412ef6096eec98bb28fd601c53a5371b23a497062635b5cdde715c23840d37f1cf328f0a2ba96260357689ae3f84a80dbdca1520df68513be1285177d3c0da664c64944de78d8b8d5864f5ac15444cd3204adc4fe487503066c18fbbef8d0515248b0a97577f5aea1d255788ed4bb66d4d56303efe135063392c312b4671963daa20e0ade262984e11263a1588eba3cf829e6131ab506e6a850aacce603e8ecfd6e794c90a772603d80fd2aad6027b34854072a0d23079252adb1ba637bbc650ed4afd35d977e1498d998020bc1c814718b48ba7378a92c56827d3c2f20daa231fa51f0a9188520e2a11149e162489f0d6dbd27cf94fd5775311d3dfbcfeb431bafc3515bbb8c4ba4488c320dca0dfec548fe9f46d8810b3f6b16bb3e3eb0ea130747d3d127c5953ca8d561f8d425a35dc3f2cd831743139fbdcada42308b524313782e23b32d5d54a265eae408623e3b2779fe60e13cf47d54dfe520f9f4e57c68aed31f78629a9074d72ab87bea993a38f95ab40df3ef01735e7d44ad365a786e0d3032f1c1dc4e6839c974185dbe63f8725e79831ebe269f94c96705639ab38d5d0700da04c6a9f686e1ea13391885287ba43cf3ccef1c2227918f15ed55441c45adca84153530bbfea3cf37adbf84831a2bfcbf0ca4a4bbd90e623789fe993dc17503ec11b1ef3049f27b27ff778af364d634a46165cda1dd8241cb88740bce74a73e7e3d656df2dee05bb561a85e64671b191ec802c5bfaca49b8168e44271cf13df756395896ff41a99654f55b6951f20d04b2007938a420218db8e37445ef3267130e288e3270b13a92596a26043e1ae84f3934cdb13363bc2843f74a0f6608a36b52c985132aa427c56b7275a864b3c76502c37b8abb8d0286b3199c78492ba8103f5a23c6cdca2292c75d7d6d7080108850807f78af3dc7e418371c6b8951bd89b79fa586af4e16096b08ac1f4dc2b1e4feaa5c040bb002b57311523197b6e2bef5b79ac9c9b4a339be6f6bf7fbe9b5c93862c87be6647949c70bb2c7e268e2ab39cbadd69de628376b3af744eeabc85b599bbdd09defacefa443e05c9b5f259a7783743fecf1a749c57cacc85703269ed67db1d8d475f6fe25d66f84a77379411ba123d98fcb3ae4eec306489a08372893616a91268ea6bf34ddbf0fdef1360ab9e82f4ac80a24e41f439af06fadc223c61f445b7261eda5e1320e269d1277631ee2245cf930244bf8c04050c514e2d59035b80827586cbfeb7da7a59c1208aa86390b9dc7a9b6ef38879ba4deea5eef47c5c98d9167594cd730abdfaf082090efe759d1b13199d739c112ae324ba24b275bf1d89867b81f4580a7ea3a8d3d07b45e2de6c1c7099de3606873b13f3083ecd1e84456c9a1b1d358075c68b1a7cf0b1f26031a2909e226f5da7877d0085b879165ec4b1d9abb7b0732ab4a6f22d9a7bbd0d494ef3f9af4903dc733fe92c6b2f557d1406d223a93e8ad6e579ebcde9c39a5652ad31335df924e5b6a09a0191821b4a0c8f886e2d7860b75ae79ad9dfbebf3500c8b9762dcd131eb5c8b866b5efb4fbfdcc5e31605c2b7d2ff8db5198a6c41bcf880065ff232ff8f84ca3f8022d3428359dc9fb19f57a6ad3f3d174d8a348879a754b37095f01d9a7f6f873798b97dfc5d7c7eaf0383b3fdccdcc11b30dbb3a0fe3186a36c4ddc9674624e38a81cca60a9bbc1b124021b61a383b7547d6af187022c133ba9d6dadf711a3af3b0255b859b214ef6c5dec592248fc94339a64f19196ca0fdad80f7f8e3d78b1f783b1f038008d0d106bd86e23e33ae5728872d42a555bb36d0e3303f0b4ab41180f4251590ee3ee244b77191c31b9f3f990f71c6e237b9dbcf7ca21c9b4c2446b856c67861785bb9edb920b8f530a7a088313ef044419a879f26db137e1557d079315844f9f60bae03d8cffa7a28bd2857a001fd5d2d999fca95ff91df0e228567f6c9ff592b77b7ccdc93a951f7e34910361a8f4fb517e1c9fb956a3bb50ddf37ed37e8d26adfc0f71e059ba95ec77a1e34e1b3420c893f89b79fc72e3a1d864fd35526727f939badc29740f5ef9c0bb9a3f72e1e08b2ef2ab366f80d8e14e03e92162736e2ccb7cde82b2af08de8a6a81c03c63e2396974ec29fb122d818a2d2d5b29d11b704d3ac3b39431099b7b2f6aae04d28a2182b55380503127e4986ee9e8d5c0c2058e09e4592d08d013a4f088e45403720160622236bd56fbd9cc240efceb1b23c19ceafec49e9d5776ca9da5f7810ac979cf6ec5f678c09257abf79c9ab55dea00054e11b62c0a0ed4363d0a96a37ae1a323aa93bb1af253885afb684ed30caf5cf3b37afe6a6463a16f34cc28b4530c6bc6281f597bea476cd9a773205b96d47ed4b0bfeeee39b7ea44ff194911bcddbb161c2c0ce9488978b99e880d8e43624dbb4a567483ed293348d752634b2f46219575175e3c249b8e4e853142b66491aa1c142e7bb558955747cf2bd61ac802a2a4784d9fe080f771dd537d0ff928b3b04029d9ac03175c2d535ad7f3c123eff30c0437b32dd9fa31b2976e369b89b79a2a95e31aee15a462c5fe25fd937ce6b0795808d16163f2cea8f19b7c83913cb4a793576aa5c0ddfb6415326d8f2be0621017616c85ee46aa768b077dbec72311cb8a0f78ba0621a277a79af6607063839a52c6825a20e1c5818b24628862aff5fbbfe87311866b9956eeea7412ef69a3e4da84699b8d8b45c74ac96c3356989fd4962ba79fc26a92488304fd9f42486266b433eeb57368d26ae91ba4e7ed812581f790314cc7f44639aa7f6775618111369e8e2d68a6ab24388824bbbe3c3b0fbfb88635c1fc3216af1af40eba3555c0b0390a18ccba9e68afbcd21ecb212aaf82846f0a55793945902c48cac5d19332e23248a464529f4cca177137c508b6b13637e7df523254f24b8343d19164174202bcb00d5fe618b760c374f69b5065b1f9acf91af95abd7eb271586cae14fc835f633aacb4cd2ecd0f0ac08b688ef4d13b8a7b4c487ad46485bde0e340309672dc38af275e6ab525971409a39eff0ad134b1674db5d1f9725874e36d8730dc034b0a596c6e0a26e521c199d3e3f86815a64d148ffc394290b19f15390934b5d0da27dc8365360511628b93ebc375d2a531e4f4cfa031eeb501afe96d201c7b6078bbceb8e5d8615599f4c613bbb81a88f4eeaa57a9c008125073a30154044c422eaf2a32cecb15aab0774bb44e52b1792d154b8036ff9224af53e023fc011ab251d47d7d76e55c5015db1926d43c56d055feca1259de53ce98a2faecb5843ce17a3e83ccf678f5d13a8321f278a670c684e62b720e1eefd0abb2e9d0a3ddea81d5eb6e380a1c22af5587daac852e93a86f5e4293c18bb26b32035c7e5ca20cd2e3eabd3de0e55092a79a42c7ccc0aef033d6683043c2a29531a2ef1f503595c0e464ac042153c4685b062275f88bab93cc017f1ef9dc6f8aeab7b0b234f1c543420324512554f1786c82b37836238a4dfdf86f97d09ae466eb39d5f3ab159e2060be309d1284be133ab40abd61468c1706f7f9e57a7bc747479693a03edc8863dd196fd7cb2721260e42f4f606389c4972c74d357e7467b61cb7f455562015d29a59c7cafe0df03f26b77bca81c2bbac8cbccf8a65190b0c4e5ca832e82ce4e11044433aa397106cafc05634ad778270d20d8a13068586bc6b582ea24524fd921a5ee22dbae5296ee86d80f12b78bba26f8b42c1401b75d5cfcc4775c5cd1cb0a9248dabc8f82d216787b2a2780f7d13a5ee8c6ab56399a8dd5db3a152677d01eb8ee98d1927ca5069e0d1ba3907971a2199ba3634b48e570dd97d93729a6c43e4f359e2d89795218d52270a338a1f511b1f008cd04553c1a89caf987fd18c329be7ac2282084ef1789615d7eb7afd2261f606d3953b8863abe57796289e7761b01c3ca0faf2291287f9ad7027d7f0876b5f77d2a7f87dfa6ad4db905d4bcad042f403824aee3b4f8d7b5ab027fe1eda9d683db24f56f694b0b10ec72ba0df40bdd6e52b4a7b8d064ad46c7490c4705c14b06ef55222435d2d6316c7dfee83d225eaa431c11a4b85b0bafbe66fda1abadaec8eabcc2f8c688a7b9cb2942597f20cfafd7520892c535bbf6359a6989a84dd89989d95b8e5222c3aca2e8b0f8881d759e450466b75b5f36b7b723b0a212edf52abd591e7e545a3974b8b31b84b523af7b47e3804b5a268c86ae0bd7c80bc6b578b79f749eccdb4a00813925ef40259ac10bbd0fc4f2fc536c30f7c1efe68a52bee22f57021d23f445211b36fee6302202c9b62c6cf9064a2df424563f9f805e51c4092482253e4c258d53b80a2d26eec9fcdc104f7124d876a3ad573a7f419b0d67a41a34dac9d8f28cf9519b9c2677c9d1e720667d5fce26091d64c6df8b46c98b58017de0d055651e8caa3a230f57aef214ae2f27fa85400e34ce7087538fb6b854a6ad534780052210b8b8c90b4de4c2afbad9f58a71770ec186cfa44b61b53876bf904972078673845ec3181caaccf11f71a8e2a502deaf144f16df1df3bd81277dfbf6e1ae5a17363725ec31759b743066fbd4cafb5eda6e09418bc375f42e0dcbd4624dbe26c5fabb77152124f400e677004fcf3c862a9b5576140cbeb800fcde4409caae06286cb643842687bc6b89738374c7c759c911d7bbec8613f1fdd996d4b970fb6f2a9290a84c34d5fea6b8006357c8e6e9d4048d5a8ec476dab0b55e8358ecf7d27c31da86681f3fa74d072b1150223eaa21c4027378a99c8a2dbdc07d4062c401e92eddfff82841292bda2798c4e2ee9e09f618cc181c91a4dbefa44c410dff5cb705ee005e3a0470c13baddf9066109797a3b51e73a0ef229796d330aee0c0160529a4ca3b39e861ee5c4a0f78619007ecdf32266c7c42f0c972b91cccc45f6f8688b2692f298721cb5da39cbca9a5adef6969a2592ba421680241f8a5384bb92e70acf79c2f41d1171dfc6e1939887b9c8ea94429bcbf3532919fdcfcd0f443d3c95515b41e3f9c84bdcd3de1fd481f98482f667f2d017e3579208341e9a225f85516c8ca133cbada77598b6f596e6151eca377fcc8029cc99a879b26d975684173c0874509117ece4136bd2d69848f858c05e8ba3421499d7fb5e3e7645fc135117d8fdd1dc46bebaedfbad4dc7cc23fad6e696fe349712cf7579b4e63b38cc7d02a4c6a33ee4117d7ccdb86ea02cd791756b2a3c516d59d39ed83a8c328823f1934731820c187624219b487ca86edcc2f61a064e4e8d17f58f4a71462f3f0cfef6be95c0eb3737616de5954096d761a51534b36d798c651541acfe2e5bed6e58c45c46e014923a342409d49e782054f2a4877332e0adb0663dce0e84df0ff0d71c4c5ef18d9cacc8b8d47c78d53fc7649bab719334601c79a345d2101e65d4c3f1616741b24fdd9f7d6569fb8de67799648b323856c2ca96ac91823aa12249934c7050505258c43763ac9b174d55fee71c7817d65056c30d7853500656040621eff9f291e9db198772451cd3d58bc9421f6191a863777edf49125543b22ba127ea3063e1989bc4635d50bb22270948ba594b2066d91e1589aa0c1f476af8b170820ae0d2409df83819579621161ba55cc8d020e1ea68cedbccc9a737746d824b06e12cd2e3730260fdc52a68b6e142bdb997d8f93f25d241360285e7372939828fd54c015ba90bb4d553558ffa2558a43098644357860785addc455bf1ef4ffa5aebb8f002eb0f6a8ddd6f45edcc7cfe88d7ae7141b9113a4ab851b5fa8ff39c7024d2b3202841266f256b1f4ce4e4ea8b83d0067555ddd56fb1f8ed6cb5ce7340cdc403fac1dd3b48c373629202c70f1f95c0001b4f94a9b4bad9e1c64429b541ff57a2f2ea0c1fb2af8f0e490fac415cc3bcdd5739f2069e2bfb873206b8c8811619f1db39386cd2dd748c6301239b4e873f3d4618a43c52ef7d8abc0ca127881dc8809dc20a8730718c64c5a132cf638ec1e3b3cedaae55763637b4c6341baeff17e14c1e3c34e5d23befdddec0c7e25f0ebfa90a0bca07a0f5446507b7fb9cf91cb3cb8d24a111f46e7dbe3ea62b384eebecaf8de49b15b17f9cf151c4c3d33220b3adcf9f87222813ff2125120e77c638ab19c1daf9631d53b3efee67c3c40c9c23495848d4c5ecafc0998bf2704bb78ade666f414e9c6bb100b90451d93397d062b741a3ef20e7e6ebebab8238e2adf415775179c866eb4a2f628924674b20f711d2515d3b024e7e0fd3af6e116cfb0030709fb9bed4a441646c03dfa0b11cb460415ccb3c0999b738a0b07d55be741f3d2646a3d9e22bbfeb46b87acb6fcc2d83cdf44b656d2269a0734259a738d83a7429bfc99da7238b9497fbb10e0f623dc1f793e3be2326a7e55677e20ebb152f71794fd70d7698d0cff1334341b3a8eee38d66a021cae6f8dfe67913fdeb8d46efd9b77f28950aee7ce112f8f0dfc04132a99d95dfa923d391e06a578056ec8d15ddfb0fe8cd2790ed55f8254c6351d471c7273c2e61c2694bf6b0a1173cdc2d422246ca5cc51a9b8669dc331fe33bfe330d5167a244cf041f8444775562eaa8ddb92e0623d4c689db0919ece87a7feeb1ddd2d034a0d884d6bc8b5fa5fc7df00cf667dfc82c9e98e9b57fbe07fed391729034c467b46d3d16ed72c8ded069a117ca2d28e71ef4c37f1b1939590af19433ff2a784ff38134aacc59719673c53ed047d79e0c36f3acc03f64458826107853ff8d02de9362ffada79feabc597d019de6afb6577dbbefcaeaee801a49b1718151690b43a4d70b2fcb73ece3e3ea5fe2a9616cc6a9a494b8c6c7121497688652eb854e7bae19af3e5ae1a8df75138ce18621d11f15054ac8971f6ac084bd90e3909b46db321916c9e3c8950f3cc8e0a10dd255bd6b359c73dfde7de6e01538d03609253968177af14d327337bc1bc4e866c75ab5b69125f9e9a758f061418110a615b97f733f899382e2a14fa0ff0d1bda3b7449e1f54362697aef064fc9dd50692ad2b9ec286a1b7c1f845b09f4b1ccd46142f9e272d018eabf76c23c26b1831762a8a2c9152ef0cc843ebba20bf979e851e76141f41e817cca0ac60c1baf239a6c920a244b0be031a3b65d76eebae15bd9d49c53dd3e4303ca379f26840901f1da886d330d8ab4408a25eaa280ddb545b968b4aa59ca6f9cecbf82984264a3232b97498be0729f3d97dbd64823393b42794ec1676244212af382c37d53d66019a5db34c3d8ac156cac852ecb1c57f0f6057815460eb9f666c368acca6dfee0d2b7bdacd2148d74cea3bbac8b528f00809f403b1964b9d199d2db93363a53732efd97c66f28cb51442dbfb1c912dce078944e02889f9395ff2655e2d7118a11848e3ca64efb5eca119240b537b5d4548a744d96962e9b2a99af73bc4d753df16fe75a08005da7773b7bef68a44f56951a3a93995c5d0516a8b2c39abd2057b3d86ba7fe511dc33292596fcc4d3c67df88812e26d3628527752a81c8ce6eba711ae7ea8748229caa2580e18937f45cbfb0041f0a9254eade41c95ef4f390db6174e6b0ba89af344c215393b627348ebf00901e343c1599c832bd2cfb751750808719f18119ab3fee6baf5fd4b8bf7c92a271ff24576984610abaf1b2297c18a2784f57d7aad26f52c0d60fa6cd2484492a1524dd14924eebf53348ee2dca0643ef47381541b91d1843817067a94bb7c79e4e08c4d81c7266afefeda4a0db21b0db629cedf13b4f6ba11e6480116a7c9780009ee0e1e2bd543dbb68bea356a6010a1e3ded4229c4d8035420833fc83337dce4bc483eec76f479f33be2d6a8fb013813952eb66bc247f37f9def7e9bea0f277f380079aeb48c192b9c2c24f909435586bcb994ae6f75eea111c8842b855f90df21e956f839aef89bb1b96899e343cc66814ac08eed4defe5e2a62c54cf83a27f0499224e02bac652aebe6529143372c483edcdbfbdd5a02106fd3651875f23724d2154c0138ace745d038d6cf6b233ae2bbb8313c8ab59dc896844da5a6610845d9147e57518ada61b7debcd66111d609587632cccb936751d099639735f09f77b2b9aa1177e95ac0f8070bcbeb01bbfb3e7a638811d919016ed628865e2405aa17f5129ea43a836a76ec7cbd943e20b1e388d47db2df3394785e27dd7efbe350ccec6057af7483303a6a588d4005c05ff7f3c90dc5b9a0f95c3c541ea40615f6b0ed6cd3787e608e7612b52199062991a63511b3055dfab25b18ab55948d9d529aff91a0d8aa7705afc187e32d1107c07623a309390f32c62e1abb830bb090a7dc4c6817b554720fce354563662d78cc2bcd0483149b304ab86502f0ad36ba2c262fa60b1b84be2499e56fd72129c0fdf1961214df8dd6c9b831109fadc7464df090aac96d221ab387b6eee3080ea0d94e195ca795b5d80c42946c0c353e39075bb1aa4f3fac36a77445cd57b11b3c7ef0e89aca911be35fbb72e1e2fff84b7ece66bb8b3dea52b0aff473475d298f730e79ff77e955856817514908baaec1b53f39f9d5dcbb1cef29536a483a1845d8538e5ec43f634914bae4f2319579adcf3f0fbb151c80b54f02be54be612f9417f916579863dc16da5af07fab05db2094c77612b445b7b9784198abbfc8387486449969118d3a83142022017d69b939d30fd6b1c5ba360c0e37f264b5d604c7ebdaf327874b22d4092dcf35aad93f0f0b3037f90000d4a1892dab827d3c2f1ddede948d330706ef17bf442f8228c168401541ea89a837f81cfffde8480e1b6014f2f5d3132ee740e04622600ad93dba41e39267f17554b29102390f8196094de19e8d966d3c03a9e3348494b54045629224612734a1304ed76d17a465c305675bf99b7aff1950dd2012e6cbe8d15a9e35a7c16081bf647e2766c7b8e9d12ef8a2c94fd7531f6d1a7ac7ce6bac028917a2a71c8eded4f3ce3be8c7d98422094e88414338fc25839a582b5965c71179ce3ddf7a645bb163f3265f4bc4ebe91ff25d86520696d237741ba73084070e23494d6a890e1ae748091a6936a3137d16dd14571f86ee118bcb17c8ecd2ed20c25cbab79a78c53fba1d5db4fdb15e6a124bdb2d41fef080e7318f78c67451beff04b58a182f592fec3feab6011be4663dca8b62ad2a8e40c716b3c8a170e1574517c44e146109d223529b0de480aa168b5a4b0522cea03dd26a684b08c58aa2e1fe41f44ba8837737a74a4681b0508d9604aa7484c5ec68ccb369c056702b03f75722033597ce5b55e5a88f14e3ee23b1c96f5d38604128cf2e901a745996f4a29d412e5ce1b4ee82cf2f248aa80f8c8f2eb7b6951f1b0910857a17edc319bc76b1a59fe05f9394a5b16a91ef15afba9ee2e115c9c9afc51a8d7c4a61cb335724855007f564fec341342d53717023118917304c2dfcf0bd79e456c94718b8388767564d6055aad1d505f36506fd6e8cb1a10f6fe8a67e772030dab78b43ec15fae0322fe991944be86cd0bd78ba00110474dec84dd9597a602c4a6c97a27f13159ff1e32fa63e28f63278b09c9d051cabf8bb49f2780d4c33ca061ff16c6288ff29d46a22e31c719dfaf6087590ea8197c066f9057e0c24b40f5eb205b730985daaf7e7d85fecd7449677c0ec1abd7111d795de6de4a6ab30cf266987dd55569f2a705ae16f51de738fcc4cc218f2cb9dab8af509ba26ed93ddc7a73028d9187527fa6d554b491faade05852eb50847affb81b58d2501989d5b4a9de4237b33bea154c4d016b08f3e1c39ee61382fb500be2123ccfcd30e37c88979a19d636c9dcf5fcec822e5558cb2939e40e1af039aa942b503c9fce95f8ff51a2977f58e4d5032f9099fb4783b7764dd1299a1a89f21c1f5e38038f219e9a4e9d6c6edaef70132ee1f2a580227b256c9139d339183e278277a9dd260bf7d8a9853c4185f9b6509ab9b67a3563ed0b9afca6f2db28258025d8c2036cc4cd9fb4f7007dca96597c26476def29191cb0c77c44df80994ad3c79833820ce2f1108d17a083fa857397d2839c37899ce2e634578b3eee0135f66cfe1aeb7bcb8732fedba16093a6e666321236f56e285058d00c1bba2db3263844e4b0ea013e7a8d370ecae9ac7a9620eaaa244b25be1eb6e71368f550736d3ca13747edfb6254c9a373aadcc30a148907d39e81bdda8a53e8644b39a1625de851fc5d759686c63c76bb162b244415175eb0ccaf3a63d4ad910369e30313bdeb175ace63ad33e400e642dbdcb472a336d82d2f818e6888e5aef472d03fd298a5d14b8da432b72e6c5f5694887a7e4e165bc41055379bd204298d65796d5351a9121f949904bd6c87510649127ef5797d432c6c759939c99026d57076db225d7d0df01ab9b9e3e1221e7e323ff752eb7988cd2b2bbfc8ae7fdd23c3f7cfd2ae2679014e986e3c01b6c66a67fbf69483ad3e89c1f2dd0867f411151e048d81212c0b5e2fbba9d317f283f97c9576180121f41856355b906a22c69869bc27729db5974c5b1257b35d186dcb4e9f7bc51dc331c0ff9baed5955e5052a6ac7743b", + "txid": "5242b51f22a7d6fe9dee237137271cde704d306a5fff6a862bffaebb6f0e7e56", + "authdigest": "66cbc7ca4eab059305da6474597d40c618546ec9d1afca9c2f9faf314113b797", + "size": 10185, + "overwintered": true, + "version": 5, + "versiongroupid": "26a7270a", + "locktime": 0, + "expiryheight": 2199295, + "vin": [ + ], + "vout": [ + ], + "vjoinsplit": [ + ], + "valueBalance": -0.00010000, + "valueBalanceZat": -10000, + "vShieldedSpend": [ + ], + "vShieldedOutput": [ + { + "cv": "a2853a37316dfc32301dfcae2583797a6dab0aaba8b08e9bd052d2d65bc66c14", + "cmu": "65dc83588d7fa63510391505b2b57455d8740a29bce1ad20f798a647b4163a5a", + "ephemeralKey": "3c9748817d696cf1e540d0ffa759092740e23c2b07415d326f9d007ba1a43bea", + "encCiphertext": "62ffe037fc83aded21e4c91722b52520a2395c23e9c1a896f4b0f12d32ae8e31833d9d95adae40f6ecf7aff52af184efd390a4c1aa76b5fb1cab6003b1a8a004016f385926661f56d38273ec2c3df7775210310a65fff5fa9ac5509f0784eefea28bdcc67b0ff69eef930335f3b9768529e2bfe733024492101f642f989de8cbf04dd66638e9317780bce47085079675b772664c8007e96597dba83ea9af22ddf07ff1c212983d4a902914431245357527294e69ea5616e720ef1e9215bbfa33ba108b8d07efff2bad1850525d7725c681761c9b8c844a5548afabf176863de7b4cde3901defc3e83d31086d3c6e6af9a5fcc3cfb38b52ac7de84f91df5e0587f7603773401a62eeef10cd3ccf4d927ef42402c32f32280abbeaac33e73ceda52089820a186e9a1adfea81453998c6bbaa0deb41bc4f94586bfee80bad25fc71abe7c6dd44bcb1a6929a0112c7e4f8fcadb9745bde9422b954f72954c4d22db48719de61f383d620935b647337f73d119d79fd208e1d5a92f0855447df5782cd4764ba91efa65d9e4ebaa34e2eccb7aac93a5b0efe0c7664f3cd9384b3ff706ad3019c907cdcfa084351c9f6a0bfa8c78c91272ca66ac86dd6e1d0d6ba9704ea7dc54f71a053dce91f844c1ca62b5ddfe6b53834f4a816b1b01460810d9b87517659f4915adf4b84783a60ecf3bd71569259f1ff90a91a0b314bd4c77976d7893bf42e5d6ad0f8df95eb6d6c69d41490be8e39b2452df3bebfc297d5b0fc97f081890390fb0727a96898585f0120a7da9a798f2032590553f724d8756c67c5b0d1c0d233", + "outCiphertext": "01c4ed60fa283994fd712aab17ca6360256fd5aef0ebc48f0256e3eda5894b53981d0d46768aefdc85b48c1525b7f134dce5d4ec2d76c03c821513f1652d9671219d744bdce5e69b9a74ca0c7c837668", + "proof": "9534b3d594e1609b3bace18608750b35a066c57f85e291d194400cb351430bbbe212abba32be071e747b7310863bd5fd989855a6567a351b288144b6e9f838c6a517db94673246ef0010b65f9c0be8aca654f6f57b83d893663cfd389ab96ce50e8077fe588c16b1b5989c6cc262e6658efb9b88ac800e49e9e5999e2651b8fff28fa77071d63790df155ed8344e2581ac5205b31d4f17bd748fcf60e35a9d6048d23c94c7aca8d4e541fda497aa268df9c173af5877a5da56d8fa2a42166900" + } + ], + "bindingSig": "c734b62e56792f6c8bed48e4f108a817e83d64d6a59e38cfdb55c0f8a89bc7507c89326266f7ac03a3941f448cb879bd792bb116d0be8876c0856a76ddec0f0c", + "orchard": { + "actions": [ + { + "cv": "e16f0338626013ee5f6037fc6a3c69fa291204039d04d17c11295ee3024aea8f", + "nullifier": "5d381e9b7eb3f938b6f9182bf4f889f1e53e30f998b1cdd23f45cfaa20aaef05", + "rk": "8248cc2e1c487fcdf54a4bc22a68a17cb6fa7b2fbf333b99feb84643d321398b", + "cmx": "675634929602126b2fb40171e514769bf82f18c267ce9cda0c24300caa9a5a36", + "ephemeralKey": "1144d3b7b9ab2243ee9811d9b2e72c8bb1d145cdfcf6b29994a969b41c47208f", + "encCiphertext": "5dba8d6d871e490e9b970afec4d8bca40ba51825cdc78cc7cde6b6f235a4105b1d1b5e2765efd753095ce770f070b02cce3316721b9345680c146c2f428c0bbca90d5a8cd0a1c4c31cbfa8ec165ea9f9c71d2d05e3cf8bae5e779786f179c45a3cd8087d820cae812aded04f8acda9068af80ea834f79f1bd03bfd66f8a19074649a85ce877df1a621a867debb423ec0d19015b326fcf6f143aba34029c1da2fc7b099378a366c38c9609ef6a9d9e175e21b0c1ab94a84e28ee7f1a00e39cb6fb59f44e4567e9f85f8f98158263c52ec433c042397c784edb07c28d2bca036f59090e819157375d610acb1993a4107b48da13a371f5383429baee209b2c0cc150fcef79a042749668ba1f89ad24a8c746142191ed0e8fd63624a331d98d50daa84ccf9043076947cf5115b9f8787acd36000c5e72c8d783b29bb28a3e46036d0a592ce8a158ee5a7ac210be72d3a6185c13645d96a8446021b64043ab8b589a20091c152e7d5a993ba94770eea988e289e1536d0d81dbc7046ca9c6d918446bf970894f073c920006681ccf6d1a3f138519c68eba0296069e42dc60f2bcd0f17c400efe4f4e87de8606606dc4fdf31494df4d454d14a440b1d9db4265c7aa9bc8683c68cb149f2cc826427575e2af82e842199a9cb9fdc7243b3bc12f1a71c37eac5cf88ba830cb95728897fa4c177a290d6b2b3814173262da14db9b4ef39fc54f888a6ffef4221ae672fb03bc78ebef479360a682ddb12ea0369a428a6c2960ff8327e9a2f5e5d98ce1eae748db8f6a4631c789b4d751d6b99c97c149a813998d44a7b", + "outCiphertext": "57ba06c8bcb8a6c73c6388cdcfeb1346cec8fee7bdebf2a2388d9722183eb2d2e0e183cdd092152ef640880f4514f3c5e836cc3a8249413500630aa8da85f9e3cd92bdadbb69a2bab8d71f0b3ec5832a", + "spendAuthSig": "8a5d14b8da432b72e6c5f5694887a7e4e165bc41055379bd204298d65796d5351a9121f949904bd6c87510649127ef5797d432c6c759939c99026d57076db225" + }, + { + "cv": "7ddbddd67b34c33b2e12a0c8468e852e4a8f7df45657e9632088aa7c6c5048a2", + "nullifier": "686019cfec33b27fc88e23759938dd55a5dff589c1c21a37da617609e9d8be37", + "rk": "dbf9bd6e84ee160fe10268171d969e4611afe9d3482ed4b132dcdd11ee516f36", + "cmx": "d512a333da20266fd984caebf4937fdfd18ed07b4a45771cf5c8c16c6b258b28", + "ephemeralKey": "9a07d136a22acc766011f366c420bafb8fc1a10e42219bede5a3d1166c525491", + "encCiphertext": "ab60bbd1f973fd3fb2e94cea888e24d5fb0adce51faeda75d62de70094d4b36d38d03cd824d284fad577c3ead4d98bcc8ceccd18174a889b22380bfcc12656e764ea0b8fe1409971283008ed02cbef89d6f544c62c3b001bfe96723fda9190deecba534d69cfa358036fdaf16127b89f925c52d4e750919ffb7182b6a8ad13d0a8e00e0b906978dd24ee11869c1a63837a80e46e1216e2e273aba07aa5b0d97558db0ba7f9ac4c89403c65f1719394e479311f5cf84746e6be6f1abcac03194aa8bf1735811198b5df90dd6cac345779c185c24beda0101b932048dc4144af664a63acc0c395052882ee1f18bd0ddf13bb583861923bc00ed5ae815b964698ca097eda1c4281e039139fa3091890244f926cc4ab773ca8a35d5263d3bb48fd6ac53a4bb4d7d60b36446dbc714c35b5e13a17c5b0c70f67207839d1f7404604aff63b2fa83a4da7dac92aac96b3f250412f8d04a9e298004313b02edefd076c67d8a1316355777814e2e1ab03690e426b672d32ff65c03c592ecce6a70e34fea2e15b9a6b4fd092d027199caf27e84e25c09380b38a5eb8985355b3259aa1d94be74269b84f953053b02ba3be9df872ae5fb2d893188575bdfe222ba267b5461a0d0be274a7d9e6ee51490d98e4cd97978804c4f0f8e9f4908fd8c102b01080f5a02b7578591e95d60f3f56d8e48514b1ce7ea6894f55a32c8ac8564985d18c6b82f8dcde5b315624e9321bdd49dd350c87907cc373c0238a79321e6250e38a0ceb2c060ecee6708c11cb30a49687da9923bcdf011f9aca27e6eb5a8477a2bae2dcff9884", + "outCiphertext": "cc2349b51a66b5179ed2d8f69e4bbba74c694194e83d04a8566228227eb732a95180c6788483d1f259d52c52fe43357656d50a1cf2902c3124d60d15fc85f0447a1203f824c1106452cfec1c92b18de0", + "spendAuthSig": "d7d0df01ab9b9e3e1221e7e323ff752eb7988cd2b2bbfc8ae7fdd23c3f7cfd2ae2679014e986e3c01b6c66a67fbf69483ad3e89c1f2dd0867f411151e048d812" + } + ], + "valueBalance": 0.00011000, + "valueBalanceZat": 11000, + "flags": { + "enableSpends": true, + "enableOutputs": true + }, + "anchor": "1cbd27436a221a53d08c4838831d1bc60ff7e93df41a51412ef6096eec98bb28", + "proof": "53a5371b23a497062635b5cdde715c23840d37f1cf328f0a2ba96260357689ae3f84a80dbdca1520df68513be1285177d3c0da664c64944de78d8b8d5864f5ac15444cd3204adc4fe487503066c18fbbef8d0515248b0a97577f5aea1d255788ed4bb66d4d56303efe135063392c312b4671963daa20e0ade262984e11263a1588eba3cf829e6131ab506e6a850aacce603e8ecfd6e794c90a772603d80fd2aad6027b34854072a0d23079252adb1ba637bbc650ed4afd35d977e1498d998020bc1c814718b48ba7378a92c56827d3c2f20daa231fa51f0a9188520e2a11149e162489f0d6dbd27cf94fd5775311d3dfbcfeb431bafc3515bbb8c4ba4488c320dca0dfec548fe9f46d8810b3f6b16bb3e3eb0ea130747d3d127c5953ca8d561f8d425a35dc3f2cd831743139fbdcada42308b524313782e23b32d5d54a265eae408623e3b2779fe60e13cf47d54dfe520f9f4e57c68aed31f78629a9074d72ab87bea993a38f95ab40df3ef01735e7d44ad365a786e0d3032f1c1dc4e6839c974185dbe63f8725e79831ebe269f94c96705639ab38d5d0700da04c6a9f686e1ea13391885287ba43cf3ccef1c2227918f15ed55441c45adca84153530bbfea3cf37adbf84831a2bfcbf0ca4a4bbd90e623789fe993dc17503ec11b1ef3049f27b27ff778af364d634a46165cda1dd8241cb88740bce74a73e7e3d656df2dee05bb561a85e64671b191ec802c5bfaca49b8168e44271cf13df756395896ff41a99654f55b6951f20d04b2007938a420218db8e37445ef3267130e288e3270b13a92596a26043e1ae84f3934cdb13363bc2843f74a0f6608a36b52c985132aa427c56b7275a864b3c76502c37b8abb8d0286b3199c78492ba8103f5a23c6cdca2292c75d7d6d7080108850807f78af3dc7e418371c6b8951bd89b79fa586af4e16096b08ac1f4dc2b1e4feaa5c040bb002b57311523197b6e2bef5b79ac9c9b4a339be6f6bf7fbe9b5c93862c87be6647949c70bb2c7e268e2ab39cbadd69de628376b3af744eeabc85b599bbdd09defacefa443e05c9b5f259a7783743fecf1a749c57cacc85703269ed67db1d8d475f6fe25d66f84a77379411ba123d98fcb3ae4eec306489a08372893616a91268ea6bf34ddbf0fdef1360ab9e82f4ac80a24e41f439af06fadc223c61f445b7261eda5e1320e269d1277631ee2245cf930244bf8c04050c514e2d59035b80827586cbfeb7da7a59c1208aa86390b9dc7a9b6ef38879ba4deea5eef47c5c98d9167594cd730abdfaf082090efe759d1b13199d739c112ae324ba24b275bf1d89867b81f4580a7ea3a8d3d07b45e2de6c1c7099de3606873b13f3083ecd1e84456c9a1b1d358075c68b1a7cf0b1f26031a2909e226f5da7877d0085b879165ec4b1d9abb7b0732ab4a6f22d9a7bbd0d494ef3f9af4903dc733fe92c6b2f557d1406d223a93e8ad6e579ebcde9c39a5652ad31335df924e5b6a09a0191821b4a0c8f886e2d7860b75ae79ad9dfbebf3500c8b9762dcd131eb5c8b866b5efb4fbfdcc5e31605c2b7d2ff8db5198a6c41bcf880065ff232ff8f84ca3f8022d3428359dc9fb19f57a6ad3f3d174d8a348879a754b37095f01d9a7f6f873798b97dfc5d7c7eaf0383b3fdccdcc11b30dbb3a0fe3186a36c4ddc9674624e38a81cca60a9bbc1b124021b61a383b7547d6af187022c133ba9d6dadf711a3af3b0255b859b214ef6c5dec592248fc94339a64f19196ca0fdad80f7f8e3d78b1f783b1f038008d0d106bd86e23e33ae5728872d42a555bb36d0e3303f0b4ab41180f4251590ee3ee244b77191c31b9f3f990f71c6e237b9dbcf7ca21c9b4c2446b856c67861785bb9edb920b8f530a7a088313ef044419a879f26db137e1557d079315844f9f60bae03d8cffa7a28bd2857a001fd5d2d999fca95ff91df0e228567f6c9ff592b77b7ccdc93a951f7e34910361a8f4fb517e1c9fb956a3bb50ddf37ed37e8d26adfc0f71e059ba95ec77a1e34e1b3420c893f89b79fc72e3a1d864fd35526727f939badc29740f5ef9c0bb9a3f72e1e08b2ef2ab366f80d8e14e03e92162736e2ccb7cde82b2af08de8a6a81c03c63e2396974ec29fb122d818a2d2d5b29d11b704d3ac3b39431099b7b2f6aae04d28a2182b55380503127e4986ee9e8d5c0c2058e09e4592d08d013a4f088e45403720160622236bd56fbd9cc240efceb1b23c19ceafec49e9d5776ca9da5f7810ac979cf6ec5f678c09257abf79c9ab55dea00054e11b62c0a0ed4363d0a96a37ae1a323aa93bb1af253885afb684ed30caf5cf3b37afe6a6463a16f34cc28b4530c6bc6281f597bea476cd9a773205b96d47ed4b0bfeeee39b7ea44ff194911bcddbb161c2c0ce9488978b99e880d8e43624dbb4a567483ed293348d752634b2f46219575175e3c249b8e4e853142b66491aa1c142e7bb558955747cf2bd61ac802a2a4784d9fe080f771dd537d0ff928b3b04029d9ac03175c2d535ad7f3c123eff30c0437b32dd9fa31b2976e369b89b79a2a95e31aee15a462c5fe25fd937ce6b0795808d16163f2cea8f19b7c83913cb4a793576aa5c0ddfb6415326d8f2be0621017616c85ee46aa768b077dbec72311cb8a0f78ba0621a277a79af6607063839a52c6825a20e1c5818b24628862aff5fbbfe87311866b9956eeea7412ef69a3e4da84699b8d8b45c74ac96c3356989fd4962ba79fc26a92488304fd9f42486266b433eeb57368d26ae91ba4e7ed812581f790314cc7f44639aa7f6775618111369e8e2d68a6ab24388824bbbe3c3b0fbfb88635c1fc3216af1af40eba3555c0b0390a18ccba9e68afbcd21ecb212aaf82846f0a55793945902c48cac5d19332e23248a464529f4cca177137c508b6b13637e7df523254f24b8343d19164174202bcb00d5fe618b760c374f69b5065b1f9acf91af95abd7eb271586cae14fc835f633aacb4cd2ecd0f0ac08b688ef4d13b8a7b4c487ad46485bde0e340309672dc38af275e6ab525971409a39eff0ad134b1674db5d1f9725874e36d8730dc034b0a596c6e0a26e521c199d3e3f86815a64d148ffc394290b19f15390934b5d0da27dc8365360511628b93ebc375d2a531e4f4cfa031eeb501afe96d201c7b6078bbceb8e5d8615599f4c613bbb81a88f4eeaa57a9c008125073a30154044c422eaf2a32cecb15aab0774bb44e52b1792d154b8036ff9224af53e023fc011ab251d47d7d76e55c5015db1926d43c56d055feca1259de53ce98a2faecb5843ce17a3e83ccf678f5d13a8321f278a670c684e62b720e1eefd0abb2e9d0a3ddea81d5eb6e380a1c22af5587daac852e93a86f5e4293c18bb26b32035c7e5ca20cd2e3eabd3de0e55092a79a42c7ccc0aef033d6683043c2a29531a2ef1f503595c0e464ac042153c4685b062275f88bab93cc017f1ef9dc6f8aeab7b0b234f1c543420324512554f1786c82b37836238a4dfdf86f97d09ae466eb39d5f3ab159e2060be309d1284be133ab40abd61468c1706f7f9e57a7bc747479693a03edc8863dd196fd7cb2721260e42f4f606389c4972c74d357e7467b61cb7f455562015d29a59c7cafe0df03f26b77bca81c2bbac8cbccf8a65190b0c4e5ca832e82ce4e11044433aa397106cafc05634ad778270d20d8a13068586bc6b582ea24524fd921a5ee22dbae5296ee86d80f12b78bba26f8b42c1401b75d5cfcc4775c5cd1cb0a9248dabc8f82d216787b2a2780f7d13a5ee8c6ab56399a8dd5db3a152677d01eb8ee98d1927ca5069e0d1ba3907971a2199ba3634b48e570dd97d93729a6c43e4f359e2d89795218d52270a338a1f511b1f008cd04553c1a89caf987fd18c329be7ac2282084ef1789615d7eb7afd2261f606d3953b8863abe57796289e7761b01c3ca0faf2291287f9ad7027d7f0876b5f77d2a7f87dfa6ad4db905d4bcad042f403824aee3b4f8d7b5ab027fe1eda9d683db24f56f694b0b10ec72ba0df40bdd6e52b4a7b8d064ad46c7490c4705c14b06ef55222435d2d6316c7dfee83d225eaa431c11a4b85b0bafbe66fda1abadaec8eabcc2f8c688a7b9cb2942597f20cfafd7520892c535bbf6359a6989a84dd89989d95b8e5222c3aca2e8b0f8881d759e450466b75b5f36b7b723b0a212edf52abd591e7e545a3974b8b31b84b523af7b47e3804b5a268c86ae0bd7c80bc6b578b79f749eccdb4a00813925ef40259ac10bbd0fc4f2fc536c30f7c1efe68a52bee22f57021d23f445211b36fee6302202c9b62c6cf9064a2df424563f9f805e51c4092482253e4c258d53b80a2d26eec9fcdc104f7124d876a3ad573a7f419b0d67a41a34dac9d8f28cf9519b9c2677c9d1e720667d5fce26091d64c6df8b46c98b58017de0d055651e8caa3a230f57aef214ae2f27fa85400e34ce7087538fb6b854a6ad534780052210b8b8c90b4de4c2afbad9f58a71770ec186cfa44b61b53876bf904972078673845ec3181caaccf11f71a8e2a502deaf144f16df1df3bd81277dfbf6e1ae5a17363725ec31759b743066fbd4cafb5eda6e09418bc375f42e0dcbd4624dbe26c5fabb77152124f400e677004fcf3c862a9b5576140cbeb800fcde4409caae06286cb643842687bc6b89738374c7c759c911d7bbec8613f1fdd996d4b970fb6f2a9290a84c34d5fea6b8006357c8e6e9d4048d5a8ec476dab0b55e8358ecf7d27c31da86681f3fa74d072b1150223eaa21c4027378a99c8a2dbdc07d4062c401e92eddfff82841292bda2798c4e2ee9e09f618cc181c91a4dbefa44c410dff5cb705ee005e3a0470c13baddf9066109797a3b51e73a0ef229796d330aee0c0160529a4ca3b39e861ee5c4a0f78619007ecdf32266c7c42f0c972b91cccc45f6f8688b2692f298721cb5da39cbca9a5adef6969a2592ba421680241f8a5384bb92e70acf79c2f41d1171dfc6e1939887b9c8ea94429bcbf3532919fdcfcd0f443d3c95515b41e3f9c84bdcd3de1fd481f98482f667f2d017e3579208341e9a225f85516c8ca133cbada77598b6f596e6151eca377fcc8029cc99a879b26d975684173c0874509117ece4136bd2d69848f858c05e8ba3421499d7fb5e3e7645fc135117d8fdd1dc46bebaedfbad4dc7cc23fad6e696fe349712cf7579b4e63b38cc7d02a4c6a33ee4117d7ccdb86ea02cd791756b2a3c516d59d39ed83a8c328823f1934731820c187624219b487ca86edcc2f61a064e4e8d17f58f4a71462f3f0cfef6be95c0eb3737616de5954096d761a51534b36d798c651541acfe2e5bed6e58c45c46e014923a342409d49e782054f2a4877332e0adb0663dce0e84df0ff0d71c4c5ef18d9cacc8b8d47c78d53fc7649bab719334601c79a345d2101e65d4c3f1616741b24fdd9f7d6569fb8de67799648b323856c2ca96ac91823aa12249934c7050505258c43763ac9b174d55fee71c7817d65056c30d7853500656040621eff9f291e9db198772451cd3d58bc9421f6191a863777edf49125543b22ba127ea3063e1989bc4635d50bb22270948ba594b2066d91e1589aa0c1f476af8b170820ae0d2409df83819579621161ba55cc8d020e1ea68cedbccc9a737746d824b06e12cd2e3730260fdc52a68b6e142bdb997d8f93f25d241360285e7372939828fd54c015ba90bb4d553558ffa2558a43098644357860785addc455bf1ef4ffa5aebb8f002eb0f6a8ddd6f45edcc7cfe88d7ae7141b9113a4ab851b5fa8ff39c7024d2b3202841266f256b1f4ce4e4ea8b83d0067555ddd56fb1f8ed6cb5ce7340cdc403fac1dd3b48c373629202c70f1f95c0001b4f94a9b4bad9e1c64429b541ff57a2f2ea0c1fb2af8f0e490fac415cc3bcdd5739f2069e2bfb873206b8c8811619f1db39386cd2dd748c6301239b4e873f3d4618a43c52ef7d8abc0ca127881dc8809dc20a8730718c64c5a132cf638ec1e3b3cedaae55763637b4c6341baeff17e14c1e3c34e5d23befdddec0c7e25f0ebfa90a0bca07a0f5446507b7fb9cf91cb3cb8d24a111f46e7dbe3ea62b384eebecaf8de49b15b17f9cf151c4c3d33220b3adcf9f87222813ff2125120e77c638ab19c1daf9631d53b3efee67c3c40c9c23495848d4c5ecafc0998bf2704bb78ade666f414e9c6bb100b90451d93397d062b741a3ef20e7e6ebebab8238e2adf415775179c866eb4a2f628924674b20f711d2515d3b024e7e0fd3af6e116cfb0030709fb9bed4a441646c03dfa0b11cb460415ccb3c0999b738a0b07d55be741f3d2646a3d9e22bbfeb46b87acb6fcc2d83cdf44b656d2269a0734259a738d83a7429bfc99da7238b9497fbb10e0f623dc1f793e3be2326a7e55677e20ebb152f71794fd70d7698d0cff1334341b3a8eee38d66a021cae6f8dfe67913fdeb8d46efd9b77f28950aee7ce112f8f0dfc04132a99d95dfa923d391e06a578056ec8d15ddfb0fe8cd2790ed55f8254c6351d471c7273c2e61c2694bf6b0a1173cdc2d422246ca5cc51a9b8669dc331fe33bfe330d5167a244cf041f8444775562eaa8ddb92e0623d4c689db0919ece87a7feeb1ddd2d034a0d884d6bc8b5fa5fc7df00cf667dfc82c9e98e9b57fbe07fed391729034c467b46d3d16ed72c8ded069a117ca2d28e71ef4c37f1b1939590af19433ff2a784ff38134aacc59719673c53ed047d79e0c36f3acc03f64458826107853ff8d02de9362ffada79feabc597d019de6afb6577dbbefcaeaee801a49b1718151690b43a4d70b2fcb73ece3e3ea5fe2a9616cc6a9a494b8c6c7121497688652eb854e7bae19af3e5ae1a8df75138ce18621d11f15054ac8971f6ac084bd90e3909b46db321916c9e3c8950f3cc8e0a10dd255bd6b359c73dfde7de6e01538d03609253968177af14d327337bc1bc4e866c75ab5b69125f9e9a758f061418110a615b97f733f899382e2a14fa0ff0d1bda3b7449e1f54362697aef064fc9dd50692ad2b9ec286a1b7c1f845b09f4b1ccd46142f9e272d018eabf76c23c26b1831762a8a2c9152ef0cc843ebba20bf979e851e76141f41e817cca0ac60c1baf239a6c920a244b0be031a3b65d76eebae15bd9d49c53dd3e4303ca379f26840901f1da886d330d8ab4408a25eaa280ddb545b968b4aa59ca6f9cecbf82984264a3232b97498be0729f3d97dbd64823393b42794ec1676244212af382c37d53d66019a5db34c3d8ac156cac852ecb1c57f0f6057815460eb9f666c368acca6dfee0d2b7bdacd2148d74cea3bbac8b528f00809f403b1964b9d199d2db93363a53732efd97c66f28cb51442dbfb1c912dce078944e02889f9395ff2655e2d7118a11848e3ca64efb5eca119240b537b5d4548a744d96962e9b2a99af73bc4d753df16fe75a08005da7773b7bef68a44f56951a3a93995c5d0516a8b2c39abd2057b3d86ba7fe511dc33292596fcc4d3c67df88812e26d3628527752a81c8ce6eba711ae7ea8748229caa2580e18937f45cbfb0041f0a9254eade41c95ef4f390db6174e6b0ba89af344c215393b627348ebf00901e343c1599c832bd2cfb751750808719f18119ab3fee6baf5fd4b8bf7c92a271ff24576984610abaf1b2297c18a2784f57d7aad26f52c0d60fa6cd2484492a1524dd14924eebf53348ee2dca0643ef47381541b91d1843817067a94bb7c79e4e08c4d81c7266afefeda4a0db21b0db629cedf13b4f6ba11e6480116a7c9780009ee0e1e2bd543dbb68bea356a6010a1e3ded4229c4d8035420833fc83337dce4bc483eec76f479f33be2d6a8fb013813952eb66bc247f37f9def7e9bea0f277f380079aeb48c192b9c2c24f909435586bcb994ae6f75eea111c8842b855f90df21e956f839aef89bb1b96899e343cc66814ac08eed4defe5e2a62c54cf83a27f0499224e02bac652aebe6529143372c483edcdbfbdd5a02106fd3651875f23724d2154c0138ace745d038d6cf6b233ae2bbb8313c8ab59dc896844da5a6610845d9147e57518ada61b7debcd66111d609587632cccb936751d099639735f09f77b2b9aa1177e95ac0f8070bcbeb01bbfb3e7a638811d919016ed628865e2405aa17f5129ea43a836a76ec7cbd943e20b1e388d47db2df3394785e27dd7efbe350ccec6057af7483303a6a588d4005c05ff7f3c90dc5b9a0f95c3c541ea40615f6b0ed6cd3787e608e7612b52199062991a63511b3055dfab25b18ab55948d9d529aff91a0d8aa7705afc187e32d1107c07623a309390f32c62e1abb830bb090a7dc4c6817b554720fce354563662d78cc2bcd0483149b304ab86502f0ad36ba2c262fa60b1b84be2499e56fd72129c0fdf1961214df8dd6c9b831109fadc7464df090aac96d221ab387b6eee3080ea0d94e195ca795b5d80c42946c0c353e39075bb1aa4f3fac36a77445cd57b11b3c7ef0e89aca911be35fbb72e1e2fff84b7ece66bb8b3dea52b0aff473475d298f730e79ff77e955856817514908baaec1b53f39f9d5dcbb1cef29536a483a1845d8538e5ec43f634914bae4f2319579adcf3f0fbb151c80b54f02be54be612f9417f916579863dc16da5af07fab05db2094c77612b445b7b9784198abbfc8387486449969118d3a83142022017d69b939d30fd6b1c5ba360c0e37f264b5d604c7ebdaf327874b22d4092dcf35aad93f0f0b3037f90000d4a1892dab827d3c2f1ddede948d330706ef17bf442f8228c168401541ea89a837f81cfffde8480e1b6014f2f5d3132ee740e04622600ad93dba41e39267f17554b29102390f8196094de19e8d966d3c03a9e3348494b54045629224612734a1304ed76d17a465c305675bf99b7aff1950dd2012e6cbe8d15a9e35a7c16081bf647e2766c7b8e9d12ef8a2c94fd7531f6d1a7ac7ce6bac028917a2a71c8eded4f3ce3be8c7d98422094e88414338fc25839a582b5965c71179ce3ddf7a645bb163f3265f4bc4ebe91ff25d86520696d237741ba73084070e23494d6a890e1ae748091a6936a3137d16dd14571f86ee118bcb17c8ecd2ed20c25cbab79a78c53fba1d5db4fdb15e6a124bdb2d41fef080e7318f78c67451beff04b58a182f592fec3feab6011be4663dca8b62ad2a8e40c716b3c8a170e1574517c44e146109d223529b0de480aa168b5a4b0522cea03dd26a684b08c58aa2e1fe41f44ba8837737a74a4681b0508d9604aa7484c5ec68ccb369c056702b03f75722033597ce5b55e5a88f14e3ee23b1c96f5d38604128cf2e901a745996f4a29d412e5ce1b4ee82cf2f248aa80f8c8f2eb7b6951f1b0910857a17edc319bc76b1a59fe05f9394a5b16a91ef15afba9ee2e115c9c9afc51a8d7c4a61cb335724855007f564fec341342d53717023118917304c2dfcf0bd79e456c94718b8388767564d6055aad1d505f36506fd6e8cb1a10f6fe8a67e772030dab78b43ec15fae0322fe991944be86cd0bd78ba00110474dec84dd9597a602c4a6c97a27f13159ff1e32fa63e28f63278b09c9d051cabf8bb49f2780d4c33ca061ff16c6288ff29d46a22e31c719dfaf6087590ea8197c066f9057e0c24b40f5eb205b730985daaf7e7d85fecd7449677c0ec1abd7111d795de6de4a6ab30cf266987dd55569f2a705ae16f51de738fcc4cc218f2cb9dab8af509ba26ed93ddc7a73028d9187527fa6d554b491faade05852eb50847affb81b58d2501989d5b4a9de4237b33bea154c4d016b08f3e1c39ee61382fb500be2123ccfcd30e37c88979a19d636c9dcf5fcec822e5558cb2939e40e1af039aa942b503c9fce95f8ff51a2977f58e4d5032f9099fb4783b7764dd1299a1a89f21c1f5e38038f219e9a4e9d6c6edaef70132ee1f2a580227b256c9139d339183e278277a9dd260bf7d8a9853c4185f9b6509ab9b67a3563ed0b9afca6f2db28258025d8c2036cc4cd9fb4f7007dca96597c26476def29191cb0c77c44df80994ad3c79833820ce2f1108d17a083fa857397d2839c37899ce2e634578b3eee0135f66cfe1aeb7bcb8732fedba16093a6e666321236f56e285058d00c1bba2db3263844e4b0ea013e7a8d370ecae9ac7a9620eaaa244b25be1eb6e71368f550736d3ca13747edfb6254c9a373aadcc30a148907d39e81bdda8a53e8644b39a1625de851fc5d759686c63c76bb162b244415175eb0ccaf3a63d4ad910369e30313bdeb175ace63ad33e400e642dbdcb472a336d82d2f818e6888e5aef472d03fd29", + "bindingSig": "12c0b5e2fbba9d317f283f97c9576180121f41856355b906a22c69869bc27729db5974c5b1257b35d186dcb4e9f7bc51dc331c0ff9baed5955e5052a6ac7743b" + }, + "blockhash": "00000000013e624a68ac92cac6d5eb05f9d031c832fb952952b80fc1a85c30a6", + "height": 2199258, + "confirmations": 12, + "time": 1692624161, + "blocktime": 1692624161 +} diff --git a/zcash-haskell.cabal b/zcash-haskell.cabal index 0b27eed..c9bb109 100644 --- a/zcash-haskell.cabal +++ b/zcash-haskell.cabal @@ -56,7 +56,8 @@ test-suite zcash-haskell-test test ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends: - base >=4.7 && <5 + aeson + , base >=4.7 && <5 , bytestring , hspec , text -- 2.34.1 From 4d2540dce179183c835ac76f700583f9e0e4d985 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Mon, 21 Aug 2023 15:58:12 -0500 Subject: [PATCH 03/17] Update dependency graph --- CHANGELOG.md | 1 + src/ZcashHaskell/Types.hs | 40 +++++++++++++++++++++++++-------------- src/ZcashHaskell/Utils.hs | 12 ------------ test/Spec.hs | 5 +++++ 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f339b8c..9646ccd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Rearranged modules for cleaner dependencies. - Upgrade to Haskell LTS 21.6 ## [0.1.0] - 2023-06-14 diff --git a/src/ZcashHaskell/Types.hs b/src/ZcashHaskell/Types.hs index 03d7d13..f03eaab 100644 --- a/src/ZcashHaskell/Types.hs +++ b/src/ZcashHaskell/Types.hs @@ -97,12 +97,12 @@ instance FromJSON ShieldedOutput where p <- obj .: "proof" pure $ ShieldedOutput - (C.pack cv) - (C.pack cmu) - (C.pack ephKey) - (C.pack encText) - (C.pack outText) - (C.pack p) + (decodeHexText cv) + (decodeHexText cmu) + (decodeHexText ephKey) + (decodeHexText encText) + (decodeHexText outText) + (decodeHexText p) -- * Orchard -- | Type to represent a Unified Full Viewing Key @@ -144,14 +144,14 @@ instance FromJSON OrchardAction where a <- obj .: "spendAuthSig" pure $ OrchardAction - (C.pack n) - (C.pack r) - (C.pack c) - (C.pack ephKey) - (C.pack encText) - (C.pack outText) - (C.pack cval) - (C.pack a) + (decodeHexText n) + (decodeHexText r) + (decodeHexText c) + (decodeHexText ephKey) + (decodeHexText encText) + (decodeHexText outText) + (decodeHexText cval) + (decodeHexText a) -- | Type to represent a decoded Orchard Action data OrchardDecodedAction = OrchardDecodedAction @@ -162,3 +162,15 @@ data OrchardDecodedAction = OrchardDecodedAction deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) deriving anyclass (Data.Structured.Show) deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct OrchardDecodedAction + +-- * Helpers +-- | Helper function to turn a hex-encoded string to bytestring +decodeHexText :: String -> BS.ByteString +decodeHexText h = BS.pack $ hexRead h + where + hexRead hexText + | null chunk = [] + | otherwise = + fromIntegral (read ("0x" <> chunk)) : hexRead (drop 2 hexText) + where + chunk = take 2 hexText diff --git a/src/ZcashHaskell/Utils.hs b/src/ZcashHaskell/Utils.hs index c56a368..5f9362b 100644 --- a/src/ZcashHaskell/Utils.hs +++ b/src/ZcashHaskell/Utils.hs @@ -16,22 +16,10 @@ import C.Zcash , rustWrapperF4Jumble , rustWrapperF4UnJumble ) - import qualified Data.ByteString as BS import Foreign.Rust.Marshall.Variable import ZcashHaskell.Types --- | Helper function to turn a hex-encoded string to bytestring -decodeHexText :: String -> BS.ByteString -decodeHexText h = BS.pack $ hexRead h - where - hexRead hexText - | null chunk = [] - | otherwise = - fromIntegral (read ("0x" <> chunk)) : hexRead (drop 2 hexText) - where - chunk = take 2 hexText - -- | Decode the given bytestring using Bech32 decodeBech32 :: BS.ByteString -> RawData decodeBech32 = withPureBorshVarBuffer . rustWrapperBech32Decode diff --git a/test/Spec.hs b/test/Spec.hs index 36def01..a9999b9 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -23,6 +23,7 @@ import ZcashHaskell.Types , RawTxResponse(..) , ShieldedOutput(s_cmu) , UnifiedFullViewingKey(..) + , decodeHexText ) import ZcashHaskell.Utils @@ -309,6 +310,10 @@ main = do let fakeUvk = "uview1u83changinga987bundchofch4ract3r5x8hqsw6vzw63n24atxpcatws82z092kryazuu6d7rayyut8m36wm4wpjy2z8r9hj48fx5pf49gw4sjrq8503qpz3vqj5hg0vg9vsqeasg5qjuyh94uyfm7v76udqcm2m0wfc25hcyqswcn56xxduq3xkgxkr0l73cjy88fdvf90eq5fda9g6x7yv7d0uckpevxg6540wc76xrc4axxvlt03ptaa2a0rektglmdy68656f3uzcdgqqyu0t7wk5cvwghyyvgqc0rp3vgu5ye4nd236ml57rjh083a2755qemf6dk6pw0qrnfm7246s8eg2hhzkzpf9h73chhng7xhmyem2sjh8rs2m9nhfcslsgenm" decodeUfvk fakeUvk `shouldBe` Nothing + describe "Decode Sapling tx" $ do + let svk = + "zxviews1qvapd723qqqqpqq09ldgykvyusthmkky2w062esx5xg3nz4m29qxcvndyx6grrhrdepu4ns88sjr3u6mfp2hhwj5hfd6y24r0f64uwq65vjrmsh9mr568kenk33fcumag6djcjywkm5v295egjuk3qdd47atprs0j33nhaaqep3uqspzp5kg4mthugvug0sc3gc83atkrgmguw9g7gkvh82tugrntf66lnvyeh6ufh4j2xt0xr2r4zujtm3qvrmd3vvnulycuwqtetg2jk384" + it "succeeds with correct key" pending describe "Decode Orchard tx" $ do let uvk = "uview1u833rp8yykd7h4druwht6xp6k8krle45fx8hqsw6vzw63n24atxpcatws82z092kryazuu6d7rayyut8m36wm4wpjy2z8r9hj48fx5pf49gw4sjrq8503qpz3vqj5hg0vg9vsqeasg5qjuyh94uyfm7v76udqcm2m0wfc25hcyqswcn56xxduq3xkgxkr0l73cjy88fdvf90eq5fda9g6x7yv7d0uckpevxg6540wc76xrc4axxvlt03ptaa2a0rektglmdy68656f3uzcdgqqyu0t7wk5cvwghyyvgqc0rp3vgu5ye4nd236ml57rjh083a2755qemf6dk6pw0qrnfm7246s8eg2hhzkzpf9h73chhng7xhmyem2sjh8rs2m9nhfcslsgenm" -- 2.34.1 From 1d8e3729a87e49b78f73e23ae81d4e3fdcb5f693 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Tue, 22 Aug 2023 15:05:40 -0500 Subject: [PATCH 04/17] Implement Sapling tx decoding --- librustzcash-wrapper/src/lib.rs | 78 ++++++++++++++++++++------------- src/C/Zcash.chs | 12 ++++- src/ZcashHaskell/Orchard.hs | 2 +- src/ZcashHaskell/Sapling.hs | 13 ++++++ src/ZcashHaskell/Types.hs | 6 +-- test/Spec.hs | 27 ++++++++++-- 6 files changed, 98 insertions(+), 40 deletions(-) diff --git a/librustzcash-wrapper/src/lib.rs b/librustzcash-wrapper/src/lib.rs index 6be1288..620a206 100644 --- a/librustzcash-wrapper/src/lib.rs +++ b/librustzcash-wrapper/src/lib.rs @@ -19,13 +19,17 @@ use haskell_ffi::{ use zcash_primitives::{ zip32::Scope as SaplingScope, transaction::components::sapling::{ + read_zkproof, GrothProofBytes, OutputDescription, CompactOutputDescription }, sapling::{ value::ValueCommitment as SaplingValueCommitment, - keys::FullViewingKey as SaplingViewingKey, + keys::{ + FullViewingKey as SaplingViewingKey, + PreparedIncomingViewingKey as SaplingPreparedIncomingViewingKey + }, note_encryption::SaplingDomain, PaymentAddress, note::ExtractedNoteCommitment as SaplingExtractedNoteCommitment @@ -311,35 +315,49 @@ pub extern "C" fn rust_wrapper_ufvk_decode( } } -//#[no_mangle] -//pub extern "C" fn rust_wrapper_sapling_note_decrypt( - //key: *const u8, - //key_len: usize, - //note: *const u8, - //note_len: usize, - //out: *mut u8, - //out_len: &mut usize - //){ - //let evk: Vec = marshall_from_haskell_var(key, key_len, RW); - //let note_input: HshieldedOutput = marshall_from_haskell_var(note,note_len,RW); - //let svk = ExtendedFullViewingKey::read(&*evk); - //match svk { - //Ok(k) => { - //let domain = SaplingDomain::for_height(MainNetwork, BlockHeight::from_u32(2000000)); - //let action: CompactOutputDescription = CompactOutputDescription { - //ephemeral_key: EphemeralKeyBytes(to_array(note_input.eph_key)), - //cmu: SaplingExtractedNoteCommitment::from_bytes(&to_array(note_input.cmu)).unwrap(), - //enc_ciphertext: to_array(note_input.enc_txt) - //}; - //let fvk = k.to_diversifiable_full_viewing_key().to_ivk(SaplingScope::External); - //let result = zcash_note_encryption::try_note_decryption(&domain, &ivk, &action); - //} - //Err(_e) => { - //let hn0 = Hnote { note: 0, recipient: vec![0], memo: vec![0] }; - //marshall_to_haskell_var(&hn0, out, out_len, RW); - //} - //} -//} +#[no_mangle] +pub extern "C" fn rust_wrapper_sapling_note_decrypt( + key: *const u8, + key_len: usize, + note: *const u8, + note_len: usize, + out: *mut u8, + out_len: &mut usize + ){ + let evk: Vec = marshall_from_haskell_var(key, key_len, RW); + let note_input: HshieldedOutput = marshall_from_haskell_var(note,note_len,RW); + let svk = ExtendedFullViewingKey::read(&*evk); + match svk { + Ok(k) => { + let domain = SaplingDomain::for_height(MainNetwork, BlockHeight::from_u32(2000000)); + let mut action_bytes = vec![0]; + action_bytes.extend(¬e_input.cv); + action_bytes.extend(¬e_input.cmu); + action_bytes.extend(¬e_input.eph_key); + action_bytes.extend(¬e_input.enc_txt); + action_bytes.extend(¬e_input.out_txt); + action_bytes.extend(¬e_input.proof); + let action2 = OutputDescription::read(&mut action_bytes.as_slice()).unwrap(); + let fvk = k.to_diversifiable_full_viewing_key().to_ivk(SaplingScope::External); + let pivk = SaplingPreparedIncomingViewingKey::new(&fvk); + let result = zcash_note_encryption::try_note_decryption(&domain, &pivk, &action2); + match result { + Some((n, r, m)) => { + let hn = Hnote {note: n.value().inner(), recipient: r.to_bytes().to_vec(), memo: m.as_slice().to_vec() }; + marshall_to_haskell_var(&hn, out, out_len, RW); + } + None => { + let hn0 = Hnote { note: 0, recipient: vec![0], memo: vec![0] }; + marshall_to_haskell_var(&hn0, out, out_len, RW); + } + } + } + Err(_e) => { + let hn0 = Hnote { note: 0, recipient: vec![0], memo: vec![0] }; + marshall_to_haskell_var(&hn0, out, out_len, RW); + } + } +} #[no_mangle] pub extern "C" fn rust_wrapper_orchard_note_decrypt( diff --git a/src/C/Zcash.chs b/src/C/Zcash.chs index 1a85cf9..3f195fe 100644 --- a/src/C/Zcash.chs +++ b/src/C/Zcash.chs @@ -72,6 +72,14 @@ import ZcashHaskell.Types -> `Bool' #} +{# fun unsafe rust_wrapper_sapling_note_decrypt as rustWrapperSaplingNoteDecode + { toBorshVar* `BS.ByteString'& + , toBorshVar* `ShieldedOutput'& + , getVarBuffer `Buffer DecodedNote'& + } + -> `()' +#} + {# fun unsafe rust_wrapper_ufvk_decode as rustWrapperUfvkDecode { toBorshVar* `BS.ByteString'& , getVarBuffer `Buffer UnifiedFullViewingKey'& @@ -82,7 +90,7 @@ import ZcashHaskell.Types {# fun unsafe rust_wrapper_orchard_note_decrypt as rustWrapperOrchardNoteDecode { toBorshVar* `BS.ByteString'& , toBorshVar* `OrchardAction'& - , getVarBuffer `Buffer OrchardDecodedAction'& + , getVarBuffer `Buffer DecodedNote'& } -> `()' - #} +#} diff --git a/src/ZcashHaskell/Orchard.hs b/src/ZcashHaskell/Orchard.hs index d6bbb56..2cc3b95 100644 --- a/src/ZcashHaskell/Orchard.hs +++ b/src/ZcashHaskell/Orchard.hs @@ -35,7 +35,7 @@ decodeUfvk str = -- | Attempts to decode the given @OrchardAction@ using the given @UnifiedFullViewingKey@. decryptOrchardAction :: - OrchardAction -> UnifiedFullViewingKey -> Maybe OrchardDecodedAction + OrchardAction -> UnifiedFullViewingKey -> Maybe DecodedNote decryptOrchardAction encAction key = case a_value decodedAction of 0 -> Nothing diff --git a/src/ZcashHaskell/Sapling.hs b/src/ZcashHaskell/Sapling.hs index ed6a81d..e0016bd 100644 --- a/src/ZcashHaskell/Sapling.hs +++ b/src/ZcashHaskell/Sapling.hs @@ -3,9 +3,12 @@ module ZcashHaskell.Sapling where import C.Zcash ( rustWrapperIsShielded , rustWrapperSaplingCheck + , rustWrapperSaplingNoteDecode , rustWrapperSaplingVkDecode ) import qualified Data.ByteString as BS +import Foreign.Rust.Marshall.Variable (withPureBorshVarBuffer) +import ZcashHaskell.Types (DecodedNote(..), ShieldedOutput) -- | Check if given bytesting is a valid encoded shielded address isValidShieldedAddress :: BS.ByteString -> Bool @@ -18,3 +21,13 @@ isValidSaplingViewingKey = rustWrapperSaplingVkDecode -- | Check if the given bytestring for the Sapling viewing key matches the second bytestring for the address matchSaplingAddress :: BS.ByteString -> BS.ByteString -> Bool matchSaplingAddress = rustWrapperSaplingCheck + +-- | Attempt to decode the given Sapling raw output with the given Sapling viewing key +decodeSaplingOutput :: BS.ByteString -> ShieldedOutput -> Maybe DecodedNote +decodeSaplingOutput key out = + case a_value decodedAction of + 0 -> Nothing + _ -> Just decodedAction + where + decodedAction = + withPureBorshVarBuffer $ rustWrapperSaplingNoteDecode key out diff --git a/src/ZcashHaskell/Types.hs b/src/ZcashHaskell/Types.hs index f03eaab..233a991 100644 --- a/src/ZcashHaskell/Types.hs +++ b/src/ZcashHaskell/Types.hs @@ -153,15 +153,15 @@ instance FromJSON OrchardAction where (decodeHexText cval) (decodeHexText a) --- | Type to represent a decoded Orchard Action -data OrchardDecodedAction = OrchardDecodedAction +-- | Type to represent a decoded note +data DecodedNote = DecodedNote { a_value :: Int64 -- ^ The amount of the transaction in _zatoshis_. , a_recipient :: BS.ByteString -- ^ The recipient Orchard receiver. , a_memo :: BS.ByteString -- ^ The decoded shielded memo field. } deriving stock (Eq, Prelude.Show, GHC.Generic) deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) deriving anyclass (Data.Structured.Show) - deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct OrchardDecodedAction + deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct DecodedNote -- * Helpers -- | Helper function to turn a hex-encoded string to bytestring diff --git a/test/Spec.hs b/test/Spec.hs index a9999b9..f9f7ecc 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -11,17 +11,18 @@ import Data.Word import Test.Hspec import ZcashHaskell.Orchard import ZcashHaskell.Sapling - ( isValidSaplingViewingKey + ( decodeSaplingOutput + , isValidSaplingViewingKey , isValidShieldedAddress , matchSaplingAddress ) import ZcashHaskell.Types ( BlockResponse(..) + , DecodedNote(..) , OrchardAction(..) - , OrchardDecodedAction(..) , RawData(..) , RawTxResponse(..) - , ShieldedOutput(s_cmu) + , ShieldedOutput(ShieldedOutput, s_cmu) , UnifiedFullViewingKey(..) , decodeHexText ) @@ -313,7 +314,25 @@ main = do describe "Decode Sapling tx" $ do let svk = "zxviews1qvapd723qqqqpqq09ldgykvyusthmkky2w062esx5xg3nz4m29qxcvndyx6grrhrdepu4ns88sjr3u6mfp2hhwj5hfd6y24r0f64uwq65vjrmsh9mr568kenk33fcumag6djcjywkm5v295egjuk3qdd47atprs0j33nhaaqep3uqspzp5kg4mthugvug0sc3gc83atkrgmguw9g7gkvh82tugrntf66lnvyeh6ufh4j2xt0xr2r4zujtm3qvrmd3vvnulycuwqtetg2jk384" - it "succeeds with correct key" pending + it "succeeds with correct key" $ do + let rawKey = decodeBech32 svk + let rawNote = + ShieldedOutput + (decodeHexText + "a2853a37316dfc32301dfcae2583797a6dab0aaba8b08e9bd052d2d65bc66c14") + (decodeHexText + "65dc83588d7fa63510391505b2b57455d8740a29bce1ad20f798a647b4163a5a") + (decodeHexText + "3c9748817d696cf1e540d0ffa759092740e23c2b07415d326f9d007ba1a43bea") + (decodeHexText + "62ffe037fc83aded21e4c91722b52520a2395c23e9c1a896f4b0f12d32ae8e31833d9d95adae40f6ecf7aff52af184efd390a4c1aa76b5fb1cab6003b1a8a004016f385926661f56d38273ec2c3df7775210310a65fff5fa9ac5509f0784eefea28bdcc67b0ff69eef930335f3b9768529e2bfe733024492101f642f989de8cbf04dd66638e9317780bce47085079675b772664c8007e96597dba83ea9af22ddf07ff1c212983d4a902914431245357527294e69ea5616e720ef1e9215bbfa33ba108b8d07efff2bad1850525d7725c681761c9b8c844a5548afabf176863de7b4cde3901defc3e83d31086d3c6e6af9a5fcc3cfb38b52ac7de84f91df5e0587f7603773401a62eeef10cd3ccf4d927ef42402c32f32280abbeaac33e73ceda52089820a186e9a1adfea81453998c6bbaa0deb41bc4f94586bfee80bad25fc71abe7c6dd44bcb1a6929a0112c7e4f8fcadb9745bde9422b954f72954c4d22db48719de61f383d620935b647337f73d119d79fd208e1d5a92f0855447df5782cd4764ba91efa65d9e4ebaa34e2eccb7aac93a5b0efe0c7664f3cd9384b3ff706ad3019c907cdcfa084351c9f6a0bfa8c78c91272ca66ac86dd6e1d0d6ba9704ea7dc54f71a053dce91f844c1ca62b5ddfe6b53834f4a816b1b01460810d9b87517659f4915adf4b84783a60ecf3bd71569259f1ff90a91a0b314bd4c77976d7893bf42e5d6ad0f8df95eb6d6c69d41490be8e39b2452df3bebfc297d5b0fc97f081890390fb0727a96898585f0120a7da9a798f2032590553f724d8756c67c5b0d1c0d233") + (decodeHexText + "01c4ed60fa283994fd712aab17ca6360256fd5aef0ebc48f0256e3eda5894b53981d0d46768aefdc85b48c1525b7f134dce5d4ec2d76c03c821513f1652d9671219d744bdce5e69b9a74ca0c7c837668") + (decodeHexText + "9534b3d594e1609b3bace18608750b35a066c57f85e291d194400cb351430bbbe212abba32be071e747b7310863bd5fd989855a6567a351b288144b6e9f838c6a517db94673246ef0010b65f9c0be8aca654f6f57b83d893663cfd389ab96ce50e8077fe588c16b1b5989c6cc262e6658efb9b88ac800e49e9e5999e2651b8fff28fa77071d63790df155ed8344e2581ac5205b31d4f17bd748fcf60e35a9d6048d23c94c7aca8d4e541fda497aa268df9c173af5877a5da56d8fa2a42166900") + print rawNote + let a = decodeSaplingOutput (bytes rawKey) rawNote + maybe 0 a_value a `shouldNotBe` 0 describe "Decode Orchard tx" $ do let uvk = "uview1u833rp8yykd7h4druwht6xp6k8krle45fx8hqsw6vzw63n24atxpcatws82z092kryazuu6d7rayyut8m36wm4wpjy2z8r9hj48fx5pf49gw4sjrq8503qpz3vqj5hg0vg9vsqeasg5qjuyh94uyfm7v76udqcm2m0wfc25hcyqswcn56xxduq3xkgxkr0l73cjy88fdvf90eq5fda9g6x7yv7d0uckpevxg6540wc76xrc4axxvlt03ptaa2a0rektglmdy68656f3uzcdgqqyu0t7wk5cvwghyyvgqc0rp3vgu5ye4nd236ml57rjh083a2755qemf6dk6pw0qrnfm7246s8eg2hhzkzpf9h73chhng7xhmyem2sjh8rs2m9nhfcslsgenm" -- 2.34.1 From 846c8971fe8c5dcf29cb9898ce3212f5a793a1cd Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Wed, 23 Aug 2023 15:19:31 -0500 Subject: [PATCH 05/17] Update Sapling decoding functions --- librustzcash-wrapper/src/lib.rs | 23 ++++++++++++----------- src/C/Zcash.chs | 2 +- src/ZcashHaskell/Sapling.hs | 6 +++--- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/librustzcash-wrapper/src/lib.rs b/librustzcash-wrapper/src/lib.rs index 620a206..bd0e0e3 100644 --- a/librustzcash-wrapper/src/lib.rs +++ b/librustzcash-wrapper/src/lib.rs @@ -1,7 +1,10 @@ use std::{ marker::PhantomData, - io::Write, + io::{ + Write, + Cursor + }, fmt::{Debug, Display, Formatter} }; @@ -34,7 +37,9 @@ use zcash_primitives::{ PaymentAddress, note::ExtractedNoteCommitment as SaplingExtractedNoteCommitment }, + transaction::Transaction, consensus::{ + BranchId::Nu5, MainNetwork, BlockHeight } @@ -325,22 +330,18 @@ pub extern "C" fn rust_wrapper_sapling_note_decrypt( out_len: &mut usize ){ let evk: Vec = marshall_from_haskell_var(key, key_len, RW); - let note_input: HshieldedOutput = marshall_from_haskell_var(note,note_len,RW); + let note_input: Vec = marshall_from_haskell_var(note,note_len,RW); + let mut note_reader = Cursor::new(note_input); let svk = ExtendedFullViewingKey::read(&*evk); match svk { Ok(k) => { let domain = SaplingDomain::for_height(MainNetwork, BlockHeight::from_u32(2000000)); - let mut action_bytes = vec![0]; - action_bytes.extend(¬e_input.cv); - action_bytes.extend(¬e_input.cmu); - action_bytes.extend(¬e_input.eph_key); - action_bytes.extend(¬e_input.enc_txt); - action_bytes.extend(¬e_input.out_txt); - action_bytes.extend(¬e_input.proof); - let action2 = OutputDescription::read(&mut action_bytes.as_slice()).unwrap(); + let action2: Transaction = Transaction::read(&mut note_reader, Nu5).unwrap(); + let bundle = action2.sapling_bundle().unwrap(); + let sh_out = bundle.shielded_outputs(); let fvk = k.to_diversifiable_full_viewing_key().to_ivk(SaplingScope::External); let pivk = SaplingPreparedIncomingViewingKey::new(&fvk); - let result = zcash_note_encryption::try_note_decryption(&domain, &pivk, &action2); + let result = zcash_note_encryption::try_note_decryption(&domain, &pivk, &sh_out[0]); match result { Some((n, r, m)) => { let hn = Hnote {note: n.value().inner(), recipient: r.to_bytes().to_vec(), memo: m.as_slice().to_vec() }; diff --git a/src/C/Zcash.chs b/src/C/Zcash.chs index 3f195fe..76e7889 100644 --- a/src/C/Zcash.chs +++ b/src/C/Zcash.chs @@ -74,7 +74,7 @@ import ZcashHaskell.Types {# fun unsafe rust_wrapper_sapling_note_decrypt as rustWrapperSaplingNoteDecode { toBorshVar* `BS.ByteString'& - , toBorshVar* `ShieldedOutput'& + , toBorshVar* `BS.ByteString'& , getVarBuffer `Buffer DecodedNote'& } -> `()' diff --git a/src/ZcashHaskell/Sapling.hs b/src/ZcashHaskell/Sapling.hs index e0016bd..a0a4716 100644 --- a/src/ZcashHaskell/Sapling.hs +++ b/src/ZcashHaskell/Sapling.hs @@ -8,7 +8,7 @@ import C.Zcash ) import qualified Data.ByteString as BS import Foreign.Rust.Marshall.Variable (withPureBorshVarBuffer) -import ZcashHaskell.Types (DecodedNote(..), ShieldedOutput) +import ZcashHaskell.Types (DecodedNote(..), ShieldedOutput(..)) -- | Check if given bytesting is a valid encoded shielded address isValidShieldedAddress :: BS.ByteString -> Bool @@ -22,8 +22,8 @@ isValidSaplingViewingKey = rustWrapperSaplingVkDecode matchSaplingAddress :: BS.ByteString -> BS.ByteString -> Bool matchSaplingAddress = rustWrapperSaplingCheck --- | Attempt to decode the given Sapling raw output with the given Sapling viewing key -decodeSaplingOutput :: BS.ByteString -> ShieldedOutput -> Maybe DecodedNote +-- | Attempt to decode the given raw tx with the given Sapling viewing key +decodeSaplingOutput :: BS.ByteString -> BS.ByteString -> Maybe DecodedNote decodeSaplingOutput key out = case a_value decodedAction of 0 -> Nothing -- 2.34.1 From e00faeda513a23f0849a4643efbb3c34650d4e20 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Wed, 23 Aug 2023 15:20:01 -0500 Subject: [PATCH 06/17] Add tests for Sapling decoding --- CHANGELOG.md | 2 ++ test/Spec.hs | 38 ++++++++++++++++++-------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9646ccd..f279786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Functions to decode Sapling transactions +- Tests for Sapling decoding - Type for block response - Type for raw transaction response - JSON parsers for block response, transaction response, `ShieldedOutput` and `OrchardAction` diff --git a/test/Spec.hs b/test/Spec.hs index f9f7ecc..ac0a218 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -22,7 +22,7 @@ import ZcashHaskell.Types , OrchardAction(..) , RawData(..) , RawTxResponse(..) - , ShieldedOutput(ShieldedOutput, s_cmu) + , ShieldedOutput(..) , UnifiedFullViewingKey(..) , decodeHexText ) @@ -314,25 +314,23 @@ main = do describe "Decode Sapling tx" $ do let svk = "zxviews1qvapd723qqqqpqq09ldgykvyusthmkky2w062esx5xg3nz4m29qxcvndyx6grrhrdepu4ns88sjr3u6mfp2hhwj5hfd6y24r0f64uwq65vjrmsh9mr568kenk33fcumag6djcjywkm5v295egjuk3qdd47atprs0j33nhaaqep3uqspzp5kg4mthugvug0sc3gc83atkrgmguw9g7gkvh82tugrntf66lnvyeh6ufh4j2xt0xr2r4zujtm3qvrmd3vvnulycuwqtetg2jk384" - it "succeeds with correct key" $ do - let rawKey = decodeBech32 svk - let rawNote = - ShieldedOutput - (decodeHexText - "a2853a37316dfc32301dfcae2583797a6dab0aaba8b08e9bd052d2d65bc66c14") - (decodeHexText - "65dc83588d7fa63510391505b2b57455d8740a29bce1ad20f798a647b4163a5a") - (decodeHexText - "3c9748817d696cf1e540d0ffa759092740e23c2b07415d326f9d007ba1a43bea") - (decodeHexText - "62ffe037fc83aded21e4c91722b52520a2395c23e9c1a896f4b0f12d32ae8e31833d9d95adae40f6ecf7aff52af184efd390a4c1aa76b5fb1cab6003b1a8a004016f385926661f56d38273ec2c3df7775210310a65fff5fa9ac5509f0784eefea28bdcc67b0ff69eef930335f3b9768529e2bfe733024492101f642f989de8cbf04dd66638e9317780bce47085079675b772664c8007e96597dba83ea9af22ddf07ff1c212983d4a902914431245357527294e69ea5616e720ef1e9215bbfa33ba108b8d07efff2bad1850525d7725c681761c9b8c844a5548afabf176863de7b4cde3901defc3e83d31086d3c6e6af9a5fcc3cfb38b52ac7de84f91df5e0587f7603773401a62eeef10cd3ccf4d927ef42402c32f32280abbeaac33e73ceda52089820a186e9a1adfea81453998c6bbaa0deb41bc4f94586bfee80bad25fc71abe7c6dd44bcb1a6929a0112c7e4f8fcadb9745bde9422b954f72954c4d22db48719de61f383d620935b647337f73d119d79fd208e1d5a92f0855447df5782cd4764ba91efa65d9e4ebaa34e2eccb7aac93a5b0efe0c7664f3cd9384b3ff706ad3019c907cdcfa084351c9f6a0bfa8c78c91272ca66ac86dd6e1d0d6ba9704ea7dc54f71a053dce91f844c1ca62b5ddfe6b53834f4a816b1b01460810d9b87517659f4915adf4b84783a60ecf3bd71569259f1ff90a91a0b314bd4c77976d7893bf42e5d6ad0f8df95eb6d6c69d41490be8e39b2452df3bebfc297d5b0fc97f081890390fb0727a96898585f0120a7da9a798f2032590553f724d8756c67c5b0d1c0d233") - (decodeHexText - "01c4ed60fa283994fd712aab17ca6360256fd5aef0ebc48f0256e3eda5894b53981d0d46768aefdc85b48c1525b7f134dce5d4ec2d76c03c821513f1652d9671219d744bdce5e69b9a74ca0c7c837668") - (decodeHexText - "9534b3d594e1609b3bace18608750b35a066c57f85e291d194400cb351430bbbe212abba32be071e747b7310863bd5fd989855a6567a351b288144b6e9f838c6a517db94673246ef0010b65f9c0be8aca654f6f57b83d893663cfd389ab96ce50e8077fe588c16b1b5989c6cc262e6658efb9b88ac800e49e9e5999e2651b8fff28fa77071d63790df155ed8344e2581ac5205b31d4f17bd748fcf60e35a9d6048d23c94c7aca8d4e541fda497aa268df9c173af5877a5da56d8fa2a42166900") - print rawNote - let a = decodeSaplingOutput (bytes rawKey) rawNote - maybe 0 a_value a `shouldNotBe` 0 + let badvk = + "zxviews1qvapd723ffakeqq09ldgykvyusthmkky2w062esx5xg3nz4m29qxcvndyx6grrhrdepu4ns88sjr3u6mfp2hhwj5hfd6y24r0f64uwq65vjrmsh9mr568kenk33fcumag6djcjywkm5v295egjuk3qdd47atprs0j33nhaaqep3uqspzp5kg4mthugvug0sc3gc83atkrgmguw9g7gkvh82tugrntf66lnvyeh6ufh4j2xt0xr2r4zujtm3qvrmd3vvnulycuwqtetg2jk384" + let rawKey = decodeBech32 svk + let badKey = decodeBech32 badvk + let rawTx = + decodeHexText + "050000800a27a726b4d0d6c200000000ff8e210000000001146cc65bd6d252d09b8eb0a8ab0aab6d7a798325aefc1d3032fc6d31373a85a25a3a16b447a698f720ade1bc290a74d85574b5b20515391035a67f8d5883dc65ea3ba4a17b009d6f325d41072b3ce240270959a7ffd040e5f16c697d8148973c62ffe037fc83aded21e4c91722b52520a2395c23e9c1a896f4b0f12d32ae8e31833d9d95adae40f6ecf7aff52af184efd390a4c1aa76b5fb1cab6003b1a8a004016f385926661f56d38273ec2c3df7775210310a65fff5fa9ac5509f0784eefea28bdcc67b0ff69eef930335f3b9768529e2bfe733024492101f642f989de8cbf04dd66638e9317780bce47085079675b772664c8007e96597dba83ea9af22ddf07ff1c212983d4a902914431245357527294e69ea5616e720ef1e9215bbfa33ba108b8d07efff2bad1850525d7725c681761c9b8c844a5548afabf176863de7b4cde3901defc3e83d31086d3c6e6af9a5fcc3cfb38b52ac7de84f91df5e0587f7603773401a62eeef10cd3ccf4d927ef42402c32f32280abbeaac33e73ceda52089820a186e9a1adfea81453998c6bbaa0deb41bc4f94586bfee80bad25fc71abe7c6dd44bcb1a6929a0112c7e4f8fcadb9745bde9422b954f72954c4d22db48719de61f383d620935b647337f73d119d79fd208e1d5a92f0855447df5782cd4764ba91efa65d9e4ebaa34e2eccb7aac93a5b0efe0c7664f3cd9384b3ff706ad3019c907cdcfa084351c9f6a0bfa8c78c91272ca66ac86dd6e1d0d6ba9704ea7dc54f71a053dce91f844c1ca62b5ddfe6b53834f4a816b1b01460810d9b87517659f4915adf4b84783a60ecf3bd71569259f1ff90a91a0b314bd4c77976d7893bf42e5d6ad0f8df95eb6d6c69d41490be8e39b2452df3bebfc297d5b0fc97f081890390fb0727a96898585f0120a7da9a798f2032590553f724d8756c67c5b0d1c0d23301c4ed60fa283994fd712aab17ca6360256fd5aef0ebc48f0256e3eda5894b53981d0d46768aefdc85b48c1525b7f134dce5d4ec2d76c03c821513f1652d9671219d744bdce5e69b9a74ca0c7c837668f0d8ffffffffffff9534b3d594e1609b3bace18608750b35a066c57f85e291d194400cb351430bbbe212abba32be071e747b7310863bd5fd989855a6567a351b288144b6e9f838c6a517db94673246ef0010b65f9c0be8aca654f6f57b83d893663cfd389ab96ce50e8077fe588c16b1b5989c6cc262e6658efb9b88ac800e49e9e5999e2651b8fff28fa77071d63790df155ed8344e2581ac5205b31d4f17bd748fcf60e35a9d6048d23c94c7aca8d4e541fda497aa268df9c173af5877a5da56d8fa2a42166900c734b62e56792f6c8bed48e4f108a817e83d64d6a59e38cfdb55c0f8a89bc7507c89326266f7ac03a3941f448cb879bd792bb116d0be8876c0856a76ddec0f0c02e16f0338626013ee5f6037fc6a3c69fa291204039d04d17c11295ee3024aea8f5d381e9b7eb3f938b6f9182bf4f889f1e53e30f998b1cdd23f45cfaa20aaef058248cc2e1c487fcdf54a4bc22a68a17cb6fa7b2fbf333b99feb84643d321398b675634929602126b2fb40171e514769bf82f18c267ce9cda0c24300caa9a5a361144d3b7b9ab2243ee9811d9b2e72c8bb1d145cdfcf6b29994a969b41c47208f5dba8d6d871e490e9b970afec4d8bca40ba51825cdc78cc7cde6b6f235a4105b1d1b5e2765efd753095ce770f070b02cce3316721b9345680c146c2f428c0bbca90d5a8cd0a1c4c31cbfa8ec165ea9f9c71d2d05e3cf8bae5e779786f179c45a3cd8087d820cae812aded04f8acda9068af80ea834f79f1bd03bfd66f8a19074649a85ce877df1a621a867debb423ec0d19015b326fcf6f143aba34029c1da2fc7b099378a366c38c9609ef6a9d9e175e21b0c1ab94a84e28ee7f1a00e39cb6fb59f44e4567e9f85f8f98158263c52ec433c042397c784edb07c28d2bca036f59090e819157375d610acb1993a4107b48da13a371f5383429baee209b2c0cc150fcef79a042749668ba1f89ad24a8c746142191ed0e8fd63624a331d98d50daa84ccf9043076947cf5115b9f8787acd36000c5e72c8d783b29bb28a3e46036d0a592ce8a158ee5a7ac210be72d3a6185c13645d96a8446021b64043ab8b589a20091c152e7d5a993ba94770eea988e289e1536d0d81dbc7046ca9c6d918446bf970894f073c920006681ccf6d1a3f138519c68eba0296069e42dc60f2bcd0f17c400efe4f4e87de8606606dc4fdf31494df4d454d14a440b1d9db4265c7aa9bc8683c68cb149f2cc826427575e2af82e842199a9cb9fdc7243b3bc12f1a71c37eac5cf88ba830cb95728897fa4c177a290d6b2b3814173262da14db9b4ef39fc54f888a6ffef4221ae672fb03bc78ebef479360a682ddb12ea0369a428a6c2960ff8327e9a2f5e5d98ce1eae748db8f6a4631c789b4d751d6b99c97c149a813998d44a7b57ba06c8bcb8a6c73c6388cdcfeb1346cec8fee7bdebf2a2388d9722183eb2d2e0e183cdd092152ef640880f4514f3c5e836cc3a8249413500630aa8da85f9e3cd92bdadbb69a2bab8d71f0b3ec5832a7ddbddd67b34c33b2e12a0c8468e852e4a8f7df45657e9632088aa7c6c5048a2686019cfec33b27fc88e23759938dd55a5dff589c1c21a37da617609e9d8be37dbf9bd6e84ee160fe10268171d969e4611afe9d3482ed4b132dcdd11ee516f36d512a333da20266fd984caebf4937fdfd18ed07b4a45771cf5c8c16c6b258b289a07d136a22acc766011f366c420bafb8fc1a10e42219bede5a3d1166c525491ab60bbd1f973fd3fb2e94cea888e24d5fb0adce51faeda75d62de70094d4b36d38d03cd824d284fad577c3ead4d98bcc8ceccd18174a889b22380bfcc12656e764ea0b8fe1409971283008ed02cbef89d6f544c62c3b001bfe96723fda9190deecba534d69cfa358036fdaf16127b89f925c52d4e750919ffb7182b6a8ad13d0a8e00e0b906978dd24ee11869c1a63837a80e46e1216e2e273aba07aa5b0d97558db0ba7f9ac4c89403c65f1719394e479311f5cf84746e6be6f1abcac03194aa8bf1735811198b5df90dd6cac345779c185c24beda0101b932048dc4144af664a63acc0c395052882ee1f18bd0ddf13bb583861923bc00ed5ae815b964698ca097eda1c4281e039139fa3091890244f926cc4ab773ca8a35d5263d3bb48fd6ac53a4bb4d7d60b36446dbc714c35b5e13a17c5b0c70f67207839d1f7404604aff63b2fa83a4da7dac92aac96b3f250412f8d04a9e298004313b02edefd076c67d8a1316355777814e2e1ab03690e426b672d32ff65c03c592ecce6a70e34fea2e15b9a6b4fd092d027199caf27e84e25c09380b38a5eb8985355b3259aa1d94be74269b84f953053b02ba3be9df872ae5fb2d893188575bdfe222ba267b5461a0d0be274a7d9e6ee51490d98e4cd97978804c4f0f8e9f4908fd8c102b01080f5a02b7578591e95d60f3f56d8e48514b1ce7ea6894f55a32c8ac8564985d18c6b82f8dcde5b315624e9321bdd49dd350c87907cc373c0238a79321e6250e38a0ceb2c060ecee6708c11cb30a49687da9923bcdf011f9aca27e6eb5a8477a2bae2dcff9884cc2349b51a66b5179ed2d8f69e4bbba74c694194e83d04a8566228227eb732a95180c6788483d1f259d52c52fe43357656d50a1cf2902c3124d60d15fc85f0447a1203f824c1106452cfec1c92b18de003f82a0000000000001cbd27436a221a53d08c4838831d1bc60ff7e93df41a51412ef6096eec98bb28fd601c53a5371b23a497062635b5cdde715c23840d37f1cf328f0a2ba96260357689ae3f84a80dbdca1520df68513be1285177d3c0da664c64944de78d8b8d5864f5ac15444cd3204adc4fe487503066c18fbbef8d0515248b0a97577f5aea1d255788ed4bb66d4d56303efe135063392c312b4671963daa20e0ade262984e11263a1588eba3cf829e6131ab506e6a850aacce603e8ecfd6e794c90a772603d80fd2aad6027b34854072a0d23079252adb1ba637bbc650ed4afd35d977e1498d998020bc1c814718b48ba7378a92c56827d3c2f20daa231fa51f0a9188520e2a11149e162489f0d6dbd27cf94fd5775311d3dfbcfeb431bafc3515bbb8c4ba4488c320dca0dfec548fe9f46d8810b3f6b16bb3e3eb0ea130747d3d127c5953ca8d561f8d425a35dc3f2cd831743139fbdcada42308b524313782e23b32d5d54a265eae408623e3b2779fe60e13cf47d54dfe520f9f4e57c68aed31f78629a9074d72ab87bea993a38f95ab40df3ef01735e7d44ad365a786e0d3032f1c1dc4e6839c974185dbe63f8725e79831ebe269f94c96705639ab38d5d0700da04c6a9f686e1ea13391885287ba43cf3ccef1c2227918f15ed55441c45adca84153530bbfea3cf37adbf84831a2bfcbf0ca4a4bbd90e623789fe993dc17503ec11b1ef3049f27b27ff778af364d634a46165cda1dd8241cb88740bce74a73e7e3d656df2dee05bb561a85e64671b191ec802c5bfaca49b8168e44271cf13df756395896ff41a99654f55b6951f20d04b2007938a420218db8e37445ef3267130e288e3270b13a92596a26043e1ae84f3934cdb13363bc2843f74a0f6608a36b52c985132aa427c56b7275a864b3c76502c37b8abb8d0286b3199c78492ba8103f5a23c6cdca2292c75d7d6d7080108850807f78af3dc7e418371c6b8951bd89b79fa586af4e16096b08ac1f4dc2b1e4feaa5c040bb002b57311523197b6e2bef5b79ac9c9b4a339be6f6bf7fbe9b5c93862c87be6647949c70bb2c7e268e2ab39cbadd69de628376b3af744eeabc85b599bbdd09defacefa443e05c9b5f259a7783743fecf1a749c57cacc85703269ed67db1d8d475f6fe25d66f84a77379411ba123d98fcb3ae4eec306489a08372893616a91268ea6bf34ddbf0fdef1360ab9e82f4ac80a24e41f439af06fadc223c61f445b7261eda5e1320e269d1277631ee2245cf930244bf8c04050c514e2d59035b80827586cbfeb7da7a59c1208aa86390b9dc7a9b6ef38879ba4deea5eef47c5c98d9167594cd730abdfaf082090efe759d1b13199d739c112ae324ba24b275bf1d89867b81f4580a7ea3a8d3d07b45e2de6c1c7099de3606873b13f3083ecd1e84456c9a1b1d358075c68b1a7cf0b1f26031a2909e226f5da7877d0085b879165ec4b1d9abb7b0732ab4a6f22d9a7bbd0d494ef3f9af4903dc733fe92c6b2f557d1406d223a93e8ad6e579ebcde9c39a5652ad31335df924e5b6a09a0191821b4a0c8f886e2d7860b75ae79ad9dfbebf3500c8b9762dcd131eb5c8b866b5efb4fbfdcc5e31605c2b7d2ff8db5198a6c41bcf880065ff232ff8f84ca3f8022d3428359dc9fb19f57a6ad3f3d174d8a348879a754b37095f01d9a7f6f873798b97dfc5d7c7eaf0383b3fdccdcc11b30dbb3a0fe3186a36c4ddc9674624e38a81cca60a9bbc1b124021b61a383b7547d6af187022c133ba9d6dadf711a3af3b0255b859b214ef6c5dec592248fc94339a64f19196ca0fdad80f7f8e3d78b1f783b1f038008d0d106bd86e23e33ae5728872d42a555bb36d0e3303f0b4ab41180f4251590ee3ee244b77191c31b9f3f990f71c6e237b9dbcf7ca21c9b4c2446b856c67861785bb9edb920b8f530a7a088313ef044419a879f26db137e1557d079315844f9f60bae03d8cffa7a28bd2857a001fd5d2d999fca95ff91df0e228567f6c9ff592b77b7ccdc93a951f7e34910361a8f4fb517e1c9fb956a3bb50ddf37ed37e8d26adfc0f71e059ba95ec77a1e34e1b3420c893f89b79fc72e3a1d864fd35526727f939badc29740f5ef9c0bb9a3f72e1e08b2ef2ab366f80d8e14e03e92162736e2ccb7cde82b2af08de8a6a81c03c63e2396974ec29fb122d818a2d2d5b29d11b704d3ac3b39431099b7b2f6aae04d28a2182b55380503127e4986ee9e8d5c0c2058e09e4592d08d013a4f088e45403720160622236bd56fbd9cc240efceb1b23c19ceafec49e9d5776ca9da5f7810ac979cf6ec5f678c09257abf79c9ab55dea00054e11b62c0a0ed4363d0a96a37ae1a323aa93bb1af253885afb684ed30caf5cf3b37afe6a6463a16f34cc28b4530c6bc6281f597bea476cd9a773205b96d47ed4b0bfeeee39b7ea44ff194911bcddbb161c2c0ce9488978b99e880d8e43624dbb4a567483ed293348d752634b2f46219575175e3c249b8e4e853142b66491aa1c142e7bb558955747cf2bd61ac802a2a4784d9fe080f771dd537d0ff928b3b04029d9ac03175c2d535ad7f3c123eff30c0437b32dd9fa31b2976e369b89b79a2a95e31aee15a462c5fe25fd937ce6b0795808d16163f2cea8f19b7c83913cb4a793576aa5c0ddfb6415326d8f2be0621017616c85ee46aa768b077dbec72311cb8a0f78ba0621a277a79af6607063839a52c6825a20e1c5818b24628862aff5fbbfe87311866b9956eeea7412ef69a3e4da84699b8d8b45c74ac96c3356989fd4962ba79fc26a92488304fd9f42486266b433eeb57368d26ae91ba4e7ed812581f790314cc7f44639aa7f6775618111369e8e2d68a6ab24388824bbbe3c3b0fbfb88635c1fc3216af1af40eba3555c0b0390a18ccba9e68afbcd21ecb212aaf82846f0a55793945902c48cac5d19332e23248a464529f4cca177137c508b6b13637e7df523254f24b8343d19164174202bcb00d5fe618b760c374f69b5065b1f9acf91af95abd7eb271586cae14fc835f633aacb4cd2ecd0f0ac08b688ef4d13b8a7b4c487ad46485bde0e340309672dc38af275e6ab525971409a39eff0ad134b1674db5d1f9725874e36d8730dc034b0a596c6e0a26e521c199d3e3f86815a64d148ffc394290b19f15390934b5d0da27dc8365360511628b93ebc375d2a531e4f4cfa031eeb501afe96d201c7b6078bbceb8e5d8615599f4c613bbb81a88f4eeaa57a9c008125073a30154044c422eaf2a32cecb15aab0774bb44e52b1792d154b8036ff9224af53e023fc011ab251d47d7d76e55c5015db1926d43c56d055feca1259de53ce98a2faecb5843ce17a3e83ccf678f5d13a8321f278a670c684e62b720e1eefd0abb2e9d0a3ddea81d5eb6e380a1c22af5587daac852e93a86f5e4293c18bb26b32035c7e5ca20cd2e3eabd3de0e55092a79a42c7ccc0aef033d6683043c2a29531a2ef1f503595c0e464ac042153c4685b062275f88bab93cc017f1ef9dc6f8aeab7b0b234f1c543420324512554f1786c82b37836238a4dfdf86f97d09ae466eb39d5f3ab159e2060be309d1284be133ab40abd61468c1706f7f9e57a7bc747479693a03edc8863dd196fd7cb2721260e42f4f606389c4972c74d357e7467b61cb7f455562015d29a59c7cafe0df03f26b77bca81c2bbac8cbccf8a65190b0c4e5ca832e82ce4e11044433aa397106cafc05634ad778270d20d8a13068586bc6b582ea24524fd921a5ee22dbae5296ee86d80f12b78bba26f8b42c1401b75d5cfcc4775c5cd1cb0a9248dabc8f82d216787b2a2780f7d13a5ee8c6ab56399a8dd5db3a152677d01eb8ee98d1927ca5069e0d1ba3907971a2199ba3634b48e570dd97d93729a6c43e4f359e2d89795218d52270a338a1f511b1f008cd04553c1a89caf987fd18c329be7ac2282084ef1789615d7eb7afd2261f606d3953b8863abe57796289e7761b01c3ca0faf2291287f9ad7027d7f0876b5f77d2a7f87dfa6ad4db905d4bcad042f403824aee3b4f8d7b5ab027fe1eda9d683db24f56f694b0b10ec72ba0df40bdd6e52b4a7b8d064ad46c7490c4705c14b06ef55222435d2d6316c7dfee83d225eaa431c11a4b85b0bafbe66fda1abadaec8eabcc2f8c688a7b9cb2942597f20cfafd7520892c535bbf6359a6989a84dd89989d95b8e5222c3aca2e8b0f8881d759e450466b75b5f36b7b723b0a212edf52abd591e7e545a3974b8b31b84b523af7b47e3804b5a268c86ae0bd7c80bc6b578b79f749eccdb4a00813925ef40259ac10bbd0fc4f2fc536c30f7c1efe68a52bee22f57021d23f445211b36fee6302202c9b62c6cf9064a2df424563f9f805e51c4092482253e4c258d53b80a2d26eec9fcdc104f7124d876a3ad573a7f419b0d67a41a34dac9d8f28cf9519b9c2677c9d1e720667d5fce26091d64c6df8b46c98b58017de0d055651e8caa3a230f57aef214ae2f27fa85400e34ce7087538fb6b854a6ad534780052210b8b8c90b4de4c2afbad9f58a71770ec186cfa44b61b53876bf904972078673845ec3181caaccf11f71a8e2a502deaf144f16df1df3bd81277dfbf6e1ae5a17363725ec31759b743066fbd4cafb5eda6e09418bc375f42e0dcbd4624dbe26c5fabb77152124f400e677004fcf3c862a9b5576140cbeb800fcde4409caae06286cb643842687bc6b89738374c7c759c911d7bbec8613f1fdd996d4b970fb6f2a9290a84c34d5fea6b8006357c8e6e9d4048d5a8ec476dab0b55e8358ecf7d27c31da86681f3fa74d072b1150223eaa21c4027378a99c8a2dbdc07d4062c401e92eddfff82841292bda2798c4e2ee9e09f618cc181c91a4dbefa44c410dff5cb705ee005e3a0470c13baddf9066109797a3b51e73a0ef229796d330aee0c0160529a4ca3b39e861ee5c4a0f78619007ecdf32266c7c42f0c972b91cccc45f6f8688b2692f298721cb5da39cbca9a5adef6969a2592ba421680241f8a5384bb92e70acf79c2f41d1171dfc6e1939887b9c8ea94429bcbf3532919fdcfcd0f443d3c95515b41e3f9c84bdcd3de1fd481f98482f667f2d017e3579208341e9a225f85516c8ca133cbada77598b6f596e6151eca377fcc8029cc99a879b26d975684173c0874509117ece4136bd2d69848f858c05e8ba3421499d7fb5e3e7645fc135117d8fdd1dc46bebaedfbad4dc7cc23fad6e696fe349712cf7579b4e63b38cc7d02a4c6a33ee4117d7ccdb86ea02cd791756b2a3c516d59d39ed83a8c328823f1934731820c187624219b487ca86edcc2f61a064e4e8d17f58f4a71462f3f0cfef6be95c0eb3737616de5954096d761a51534b36d798c651541acfe2e5bed6e58c45c46e014923a342409d49e782054f2a4877332e0adb0663dce0e84df0ff0d71c4c5ef18d9cacc8b8d47c78d53fc7649bab719334601c79a345d2101e65d4c3f1616741b24fdd9f7d6569fb8de67799648b323856c2ca96ac91823aa12249934c7050505258c43763ac9b174d55fee71c7817d65056c30d7853500656040621eff9f291e9db198772451cd3d58bc9421f6191a863777edf49125543b22ba127ea3063e1989bc4635d50bb22270948ba594b2066d91e1589aa0c1f476af8b170820ae0d2409df83819579621161ba55cc8d020e1ea68cedbccc9a737746d824b06e12cd2e3730260fdc52a68b6e142bdb997d8f93f25d241360285e7372939828fd54c015ba90bb4d553558ffa2558a43098644357860785addc455bf1ef4ffa5aebb8f002eb0f6a8ddd6f45edcc7cfe88d7ae7141b9113a4ab851b5fa8ff39c7024d2b3202841266f256b1f4ce4e4ea8b83d0067555ddd56fb1f8ed6cb5ce7340cdc403fac1dd3b48c373629202c70f1f95c0001b4f94a9b4bad9e1c64429b541ff57a2f2ea0c1fb2af8f0e490fac415cc3bcdd5739f2069e2bfb873206b8c8811619f1db39386cd2dd748c6301239b4e873f3d4618a43c52ef7d8abc0ca127881dc8809dc20a8730718c64c5a132cf638ec1e3b3cedaae55763637b4c6341baeff17e14c1e3c34e5d23befdddec0c7e25f0ebfa90a0bca07a0f5446507b7fb9cf91cb3cb8d24a111f46e7dbe3ea62b384eebecaf8de49b15b17f9cf151c4c3d33220b3adcf9f87222813ff2125120e77c638ab19c1daf9631d53b3efee67c3c40c9c23495848d4c5ecafc0998bf2704bb78ade666f414e9c6bb100b90451d93397d062b741a3ef20e7e6ebebab8238e2adf415775179c866eb4a2f628924674b20f711d2515d3b024e7e0fd3af6e116cfb0030709fb9bed4a441646c03dfa0b11cb460415ccb3c0999b738a0b07d55be741f3d2646a3d9e22bbfeb46b87acb6fcc2d83cdf44b656d2269a0734259a738d83a7429bfc99da7238b9497fbb10e0f623dc1f793e3be2326a7e55677e20ebb152f71794fd70d7698d0cff1334341b3a8eee38d66a021cae6f8dfe67913fdeb8d46efd9b77f28950aee7ce112f8f0dfc04132a99d95dfa923d391e06a578056ec8d15ddfb0fe8cd2790ed55f8254c6351d471c7273c2e61c2694bf6b0a1173cdc2d422246ca5cc51a9b8669dc331fe33bfe330d5167a244cf041f8444775562eaa8ddb92e0623d4c689db0919ece87a7feeb1ddd2d034a0d884d6bc8b5fa5fc7df00cf667dfc82c9e98e9b57fbe07fed391729034c467b46d3d16ed72c8ded069a117ca2d28e71ef4c37f1b1939590af19433ff2a784ff38134aacc59719673c53ed047d79e0c36f3acc03f64458826107853ff8d02de9362ffada79feabc597d019de6afb6577dbbefcaeaee801a49b1718151690b43a4d70b2fcb73ece3e3ea5fe2a9616cc6a9a494b8c6c7121497688652eb854e7bae19af3e5ae1a8df75138ce18621d11f15054ac8971f6ac084bd90e3909b46db321916c9e3c8950f3cc8e0a10dd255bd6b359c73dfde7de6e01538d03609253968177af14d327337bc1bc4e866c75ab5b69125f9e9a758f061418110a615b97f733f899382e2a14fa0ff0d1bda3b7449e1f54362697aef064fc9dd50692ad2b9ec286a1b7c1f845b09f4b1ccd46142f9e272d018eabf76c23c26b1831762a8a2c9152ef0cc843ebba20bf979e851e76141f41e817cca0ac60c1baf239a6c920a244b0be031a3b65d76eebae15bd9d49c53dd3e4303ca379f26840901f1da886d330d8ab4408a25eaa280ddb545b968b4aa59ca6f9cecbf82984264a3232b97498be0729f3d97dbd64823393b42794ec1676244212af382c37d53d66019a5db34c3d8ac156cac852ecb1c57f0f6057815460eb9f666c368acca6dfee0d2b7bdacd2148d74cea3bbac8b528f00809f403b1964b9d199d2db93363a53732efd97c66f28cb51442dbfb1c912dce078944e02889f9395ff2655e2d7118a11848e3ca64efb5eca119240b537b5d4548a744d96962e9b2a99af73bc4d753df16fe75a08005da7773b7bef68a44f56951a3a93995c5d0516a8b2c39abd2057b3d86ba7fe511dc33292596fcc4d3c67df88812e26d3628527752a81c8ce6eba711ae7ea8748229caa2580e18937f45cbfb0041f0a9254eade41c95ef4f390db6174e6b0ba89af344c215393b627348ebf00901e343c1599c832bd2cfb751750808719f18119ab3fee6baf5fd4b8bf7c92a271ff24576984610abaf1b2297c18a2784f57d7aad26f52c0d60fa6cd2484492a1524dd14924eebf53348ee2dca0643ef47381541b91d1843817067a94bb7c79e4e08c4d81c7266afefeda4a0db21b0db629cedf13b4f6ba11e6480116a7c9780009ee0e1e2bd543dbb68bea356a6010a1e3ded4229c4d8035420833fc83337dce4bc483eec76f479f33be2d6a8fb013813952eb66bc247f37f9def7e9bea0f277f380079aeb48c192b9c2c24f909435586bcb994ae6f75eea111c8842b855f90df21e956f839aef89bb1b96899e343cc66814ac08eed4defe5e2a62c54cf83a27f0499224e02bac652aebe6529143372c483edcdbfbdd5a02106fd3651875f23724d2154c0138ace745d038d6cf6b233ae2bbb8313c8ab59dc896844da5a6610845d9147e57518ada61b7debcd66111d609587632cccb936751d099639735f09f77b2b9aa1177e95ac0f8070bcbeb01bbfb3e7a638811d919016ed628865e2405aa17f5129ea43a836a76ec7cbd943e20b1e388d47db2df3394785e27dd7efbe350ccec6057af7483303a6a588d4005c05ff7f3c90dc5b9a0f95c3c541ea40615f6b0ed6cd3787e608e7612b52199062991a63511b3055dfab25b18ab55948d9d529aff91a0d8aa7705afc187e32d1107c07623a309390f32c62e1abb830bb090a7dc4c6817b554720fce354563662d78cc2bcd0483149b304ab86502f0ad36ba2c262fa60b1b84be2499e56fd72129c0fdf1961214df8dd6c9b831109fadc7464df090aac96d221ab387b6eee3080ea0d94e195ca795b5d80c42946c0c353e39075bb1aa4f3fac36a77445cd57b11b3c7ef0e89aca911be35fbb72e1e2fff84b7ece66bb8b3dea52b0aff473475d298f730e79ff77e955856817514908baaec1b53f39f9d5dcbb1cef29536a483a1845d8538e5ec43f634914bae4f2319579adcf3f0fbb151c80b54f02be54be612f9417f916579863dc16da5af07fab05db2094c77612b445b7b9784198abbfc8387486449969118d3a83142022017d69b939d30fd6b1c5ba360c0e37f264b5d604c7ebdaf327874b22d4092dcf35aad93f0f0b3037f90000d4a1892dab827d3c2f1ddede948d330706ef17bf442f8228c168401541ea89a837f81cfffde8480e1b6014f2f5d3132ee740e04622600ad93dba41e39267f17554b29102390f8196094de19e8d966d3c03a9e3348494b54045629224612734a1304ed76d17a465c305675bf99b7aff1950dd2012e6cbe8d15a9e35a7c16081bf647e2766c7b8e9d12ef8a2c94fd7531f6d1a7ac7ce6bac028917a2a71c8eded4f3ce3be8c7d98422094e88414338fc25839a582b5965c71179ce3ddf7a645bb163f3265f4bc4ebe91ff25d86520696d237741ba73084070e23494d6a890e1ae748091a6936a3137d16dd14571f86ee118bcb17c8ecd2ed20c25cbab79a78c53fba1d5db4fdb15e6a124bdb2d41fef080e7318f78c67451beff04b58a182f592fec3feab6011be4663dca8b62ad2a8e40c716b3c8a170e1574517c44e146109d223529b0de480aa168b5a4b0522cea03dd26a684b08c58aa2e1fe41f44ba8837737a74a4681b0508d9604aa7484c5ec68ccb369c056702b03f75722033597ce5b55e5a88f14e3ee23b1c96f5d38604128cf2e901a745996f4a29d412e5ce1b4ee82cf2f248aa80f8c8f2eb7b6951f1b0910857a17edc319bc76b1a59fe05f9394a5b16a91ef15afba9ee2e115c9c9afc51a8d7c4a61cb335724855007f564fec341342d53717023118917304c2dfcf0bd79e456c94718b8388767564d6055aad1d505f36506fd6e8cb1a10f6fe8a67e772030dab78b43ec15fae0322fe991944be86cd0bd78ba00110474dec84dd9597a602c4a6c97a27f13159ff1e32fa63e28f63278b09c9d051cabf8bb49f2780d4c33ca061ff16c6288ff29d46a22e31c719dfaf6087590ea8197c066f9057e0c24b40f5eb205b730985daaf7e7d85fecd7449677c0ec1abd7111d795de6de4a6ab30cf266987dd55569f2a705ae16f51de738fcc4cc218f2cb9dab8af509ba26ed93ddc7a73028d9187527fa6d554b491faade05852eb50847affb81b58d2501989d5b4a9de4237b33bea154c4d016b08f3e1c39ee61382fb500be2123ccfcd30e37c88979a19d636c9dcf5fcec822e5558cb2939e40e1af039aa942b503c9fce95f8ff51a2977f58e4d5032f9099fb4783b7764dd1299a1a89f21c1f5e38038f219e9a4e9d6c6edaef70132ee1f2a580227b256c9139d339183e278277a9dd260bf7d8a9853c4185f9b6509ab9b67a3563ed0b9afca6f2db28258025d8c2036cc4cd9fb4f7007dca96597c26476def29191cb0c77c44df80994ad3c79833820ce2f1108d17a083fa857397d2839c37899ce2e634578b3eee0135f66cfe1aeb7bcb8732fedba16093a6e666321236f56e285058d00c1bba2db3263844e4b0ea013e7a8d370ecae9ac7a9620eaaa244b25be1eb6e71368f550736d3ca13747edfb6254c9a373aadcc30a148907d39e81bdda8a53e8644b39a1625de851fc5d759686c63c76bb162b244415175eb0ccaf3a63d4ad910369e30313bdeb175ace63ad33e400e642dbdcb472a336d82d2f818e6888e5aef472d03fd298a5d14b8da432b72e6c5f5694887a7e4e165bc41055379bd204298d65796d5351a9121f949904bd6c87510649127ef5797d432c6c759939c99026d57076db225d7d0df01ab9b9e3e1221e7e323ff752eb7988cd2b2bbfc8ae7fdd23c3f7cfd2ae2679014e986e3c01b6c66a67fbf69483ad3e89c1f2dd0867f411151e048d81212c0b5e2fbba9d317f283f97c9576180121f41856355b906a22c69869bc27729db5974c5b1257b35d186dcb4e9f7bc51dc331c0ff9baed5955e5052a6ac7743b" + describe "succeeds with correct key" $ do + let a = decodeSaplingOutput (bytes rawKey) rawTx + it "amount should match" $ do maybe 0 a_value a `shouldBe` 10000 + it "memo should match" $ do + maybe "" a_memo a `shouldBe` "Tx with Sapling and Orchard" + describe "fails with incorrect key" $ do + let a = decodeSaplingOutput (bytes badKey) rawTx + it "amount should not match" $ do maybe 0 a_value a `shouldNotBe` 10000 + it "memo should not match" $ do + maybe "" a_memo a `shouldNotBe` "Tx with Sapling and Orchard" describe "Decode Orchard tx" $ do let uvk = "uview1u833rp8yykd7h4druwht6xp6k8krle45fx8hqsw6vzw63n24atxpcatws82z092kryazuu6d7rayyut8m36wm4wpjy2z8r9hj48fx5pf49gw4sjrq8503qpz3vqj5hg0vg9vsqeasg5qjuyh94uyfm7v76udqcm2m0wfc25hcyqswcn56xxduq3xkgxkr0l73cjy88fdvf90eq5fda9g6x7yv7d0uckpevxg6540wc76xrc4axxvlt03ptaa2a0rektglmdy68656f3uzcdgqqyu0t7wk5cvwghyyvgqc0rp3vgu5ye4nd236ml57rjh083a2755qemf6dk6pw0qrnfm7246s8eg2hhzkzpf9h73chhng7xhmyem2sjh8rs2m9nhfcslsgenm" -- 2.34.1 From 90b0b3e9545411bb81efd884b8c623995fd9edd0 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Mon, 25 Sep 2023 07:51:14 -0500 Subject: [PATCH 07/17] Add tx hex field parsing --- librustzcash-wrapper/src/lib.rs | 87 +++++++++++++++++++++++++++++++++ src/C/Zcash.chs | 1 + src/ZcashHaskell/Types.hs | 4 +- 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/librustzcash-wrapper/src/lib.rs b/librustzcash-wrapper/src/lib.rs index bd0e0e3..10bf8d6 100644 --- a/librustzcash-wrapper/src/lib.rs +++ b/librustzcash-wrapper/src/lib.rs @@ -95,6 +95,20 @@ impl ToHaskell for RawData { //} //} +#[derive(BorshSerialize, BorshDeserialize)] +pub struct HrawTx { + bytes: Vec, + s: bool, + o: bool +} + +impl ToHaskell for HrawTx { + fn to_haskell(&self, writer: &mut W, _tag: PhantomData) -> Result<()> { + self.serialize(writer)?; + Ok(()) + } +} + #[derive(BorshSerialize, BorshDeserialize)] pub struct HshieldedOutput { cv: Vec, @@ -112,6 +126,20 @@ impl FromHaskell for HshieldedOutput { } } +impl ToHaskell for HshieldedOutput { + fn to_haskell(&self, writer: &mut W, _tag: PhantomData) -> Result<()> { + self.serialize(writer)?; + Ok(()) + } +} + +impl HshieldedOutput { + fn from_object(s: OutputDescription) -> Result { + let o = HshieldedOutput { cv: s.cv().to_bytes().to_vec(), cmu: s.cmu().to_bytes().to_vec(), eph_key: s.ephemeral_key().0.to_vec(), enc_txt: s.enc_ciphertext().to_vec(), out_txt: s.out_ciphertext().to_vec(), proof: s.zkproof().to_vec() }; + Ok(o) + } +} + #[derive(BorshSerialize, BorshDeserialize)] pub struct Haction { nf: Vec, @@ -403,3 +431,62 @@ pub extern "C" fn rust_wrapper_orchard_note_decrypt( } } } + +#[no_mangle] +pub extern "C" fn rust_wrapper_tx_parse( + tx: *const u8, + tx_len: usize, + out: *mut u8, + out_len: &mut usize + ){ + let tx_input: Vec = marshall_from_haskell_var(tx, tx_len, RW); + let tx_bytes: Vec = tx_input.clone(); + let mut tx_reader = Cursor::new(tx_input); + let s_o = false; + let o_a = false; + let parsed_tx = Transaction::read(&mut tx_reader, Nu5); + match parsed_tx { + Ok(t) => { + let s_bundle = t.sapling_bundle(); + let o_bundle = t.orchard_bundle(); + match s_bundle { + Some(sb) => { + let s_o = true; + match o_bundle { + Some(ob) => { + let o_a = true; + let x = HrawTx { bytes: tx_bytes, s: s_o, o: o_a}; + marshall_to_haskell_var(&x, out, out_len, RW); + }, + None => { + let o_a = false; + let x = HrawTx { bytes: tx_bytes, s: s_o, o: o_a}; + marshall_to_haskell_var(&x, out, out_len, RW); + } + } + + }, + None => { + let s_o = false; + match o_bundle { + Some(ob) => { + let o_a = true; + let x = HrawTx { bytes: tx_bytes, s: s_o, o: o_a}; + marshall_to_haskell_var(&x, out, out_len, RW); + }, + None => { + let o_a = false; + let x = HrawTx { bytes: tx_bytes, s: s_o, o: o_a}; + marshall_to_haskell_var(&x, out, out_len, RW); + } + } + } + } + + }, + Err(_e) => { + let y = HrawTx { bytes: vec![0], s: false, o: false}; + marshall_to_haskell_var(&y, out, out_len, RW); + } + } +} diff --git a/src/C/Zcash.chs b/src/C/Zcash.chs index 76e7889..9ce0ea2 100644 --- a/src/C/Zcash.chs +++ b/src/C/Zcash.chs @@ -94,3 +94,4 @@ import ZcashHaskell.Types } -> `()' #} + diff --git a/src/ZcashHaskell/Types.hs b/src/ZcashHaskell/Types.hs index 233a991..0824ea1 100644 --- a/src/ZcashHaskell/Types.hs +++ b/src/ZcashHaskell/Types.hs @@ -59,6 +59,7 @@ instance FromJSON BlockResponse where -- | Type to represent response from the `zcashd` RPC `getrawtransaction` data RawTxResponse = RawTxResponse { rt_id :: T.Text + , rt_hex :: BS.ByteString , rt_shieldedOutputs :: [ShieldedOutput] , rt_orchardActions :: [OrchardAction] } deriving (Prelude.Show, Eq) @@ -69,8 +70,9 @@ instance FromJSON RawTxResponse where i <- obj .: "txid" s <- obj .: "vShieldedOutput" o <- obj .: "orchard" + h <- obj .: "hex" a <- o .: "actions" - pure $ RawTxResponse i s a + pure $ RawTxResponse i (decodeHexText h) s a -- * Sapling -- | Type to represent a Sapling Shielded Output as provided by the @getrawtransaction@ RPC method of @zcashd@. -- 2.34.1 From c4799c3558175dd2b5bdb80e618cb3143e1c41b6 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Tue, 26 Sep 2023 15:24:18 -0500 Subject: [PATCH 08/17] Changes for use of viewing keys --- package.yaml | 1 + src/ZcashHaskell/Orchard.hs | 4 ++-- src/ZcashHaskell/Types.hs | 43 +++++++++++++++++++++++++++++++++++++ src/ZcashHaskell/Utils.hs | 23 ++++++++++++++++++++ zcash-haskell.cabal | 1 + 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 5f7495e..eea70bb 100644 --- a/package.yaml +++ b/package.yaml @@ -32,6 +32,7 @@ library: - foreign-rust - generics-sop - aeson + - http-conduit pkg-config-dependencies: - rustzcash_wrapper-uninstalled diff --git a/src/ZcashHaskell/Orchard.hs b/src/ZcashHaskell/Orchard.hs index 2cc3b95..edee2ff 100644 --- a/src/ZcashHaskell/Orchard.hs +++ b/src/ZcashHaskell/Orchard.hs @@ -35,8 +35,8 @@ decodeUfvk str = -- | Attempts to decode the given @OrchardAction@ using the given @UnifiedFullViewingKey@. decryptOrchardAction :: - OrchardAction -> UnifiedFullViewingKey -> Maybe DecodedNote -decryptOrchardAction encAction key = + UnifiedFullViewingKey -> OrchardAction -> Maybe DecodedNote +decryptOrchardAction key encAction = case a_value decodedAction of 0 -> Nothing _ -> Just decodedAction diff --git a/src/ZcashHaskell/Types.hs b/src/ZcashHaskell/Types.hs index 0824ea1..1c5d680 100644 --- a/src/ZcashHaskell/Types.hs +++ b/src/ZcashHaskell/Types.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingVia #-} {-# LANGUAGE UndecidableInstances #-} @@ -39,6 +40,48 @@ data RawData = RawData deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct RawData -- * `zcashd` RPC +-- | A type to model Zcash RPC calls +data RpcCall = RpcCall + { jsonrpc :: T.Text + , callId :: T.Text + , method :: T.Text + , parameters :: [Data.Aeson.Value] + } deriving stock (Prelude.Show, GHC.Generic) + +instance ToJSON RpcCall where + toJSON (RpcCall j c m p) = + object ["jsonrpc" .= j, "id" .= c, "method" .= m, "params" .= p] + +-- | A type to model the response of the Zcash RPC +data RpcResponse r = MakeRpcResponse + { err :: Maybe RpcError + , respId :: T.Text + , result :: Maybe r + } deriving stock (Prelude.Show, GHC.Generic) + deriving anyclass (ToJSON) + +instance (FromJSON r) => FromJSON (RpcResponse r) where + parseJSON = + withObject "RpcResponse" $ \obj -> do + e <- obj .: "error" + i <- obj .: "id" + r <- obj .: "result" + pure $ MakeRpcResponse e i r + +-- | A type to model the errors from the Zcash RPC +data RpcError = RpcError + { ecode :: Double + , emessage :: T.Text + } deriving stock (Prelude.Show, GHC.Generic) + deriving anyclass (ToJSON) + +instance FromJSON RpcError where + parseJSON = + withObject "RpcError" $ \obj -> do + c <- obj .: "code" + m <- obj .: "message" + pure $ RpcError c m + -- | Type to represent response from the `zcashd` RPC `getblock` method data BlockResponse = BlockResponse { bl_confirmations :: Integer -- ^ Block confirmations diff --git a/src/ZcashHaskell/Utils.hs b/src/ZcashHaskell/Utils.hs index 5f9362b..10269b6 100644 --- a/src/ZcashHaskell/Utils.hs +++ b/src/ZcashHaskell/Utils.hs @@ -9,6 +9,8 @@ -- -- A set of functions to assist in the handling of elements of the Zcash protocol, allowing for decoding of memos, addresses and viewing keys. -- +{-# LANGUAGE OverloadedStrings #-} + module ZcashHaskell.Utils where import C.Zcash @@ -16,8 +18,12 @@ import C.Zcash , rustWrapperF4Jumble , rustWrapperF4UnJumble ) +import Control.Monad.IO.Class +import Data.Aeson import qualified Data.ByteString as BS +import qualified Data.Text as T import Foreign.Rust.Marshall.Variable +import Network.HTTP.Simple import ZcashHaskell.Types -- | Decode the given bytestring using Bech32 @@ -31,3 +37,20 @@ f4Jumble = withPureBorshVarBuffer . rustWrapperF4Jumble -- | Apply the inverse F4Jumble transformation to the given bytestring f4UnJumble :: BS.ByteString -> BS.ByteString f4UnJumble = withPureBorshVarBuffer . rustWrapperF4UnJumble + +-- | Make a Zcash RPC call +makeZcashCall :: + (MonadIO m, FromJSON a) + => BS.ByteString + -> BS.ByteString + -> T.Text + -> [Data.Aeson.Value] + -> m (Response a) +makeZcashCall username password m p = do + let payload = RpcCall "1.0" "test" m p + let myRequest = + setRequestBodyJSON payload $ + setRequestPort 8232 $ + setRequestBasicAuth username password $ + setRequestMethod "POST" defaultRequest + httpJSON myRequest diff --git a/zcash-haskell.cabal b/zcash-haskell.cabal index c9bb109..c99093c 100644 --- a/zcash-haskell.cabal +++ b/zcash-haskell.cabal @@ -44,6 +44,7 @@ library , bytestring , foreign-rust , generics-sop + , http-conduit , text default-language: Haskell2010 -- 2.34.1 From 489d3d632f48b40d83d35ab2b33ceb01184f4c51 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Wed, 27 Sep 2023 10:37:53 -0500 Subject: [PATCH 09/17] Add extraction of shielded outputs --- librustzcash-wrapper/src/lib.rs | 50 ++++++++------------------------- src/C/Zcash.chs | 8 +++++- src/ZcashHaskell/Sapling.hs | 4 +++ test/Spec.hs | 12 +++++--- 4 files changed, 30 insertions(+), 44 deletions(-) diff --git a/librustzcash-wrapper/src/lib.rs b/librustzcash-wrapper/src/lib.rs index 10bf8d6..cf4f370 100644 --- a/librustzcash-wrapper/src/lib.rs +++ b/librustzcash-wrapper/src/lib.rs @@ -349,7 +349,7 @@ pub extern "C" fn rust_wrapper_ufvk_decode( } #[no_mangle] -pub extern "C" fn rust_wrapper_sapling_note_decrypt( +pub extern "C" fn rust_wrapper_sapling_note_decrypt_v2( key: *const u8, key_len: usize, note: *const u8, @@ -364,12 +364,10 @@ pub extern "C" fn rust_wrapper_sapling_note_decrypt( match svk { Ok(k) => { let domain = SaplingDomain::for_height(MainNetwork, BlockHeight::from_u32(2000000)); - let action2: Transaction = Transaction::read(&mut note_reader, Nu5).unwrap(); - let bundle = action2.sapling_bundle().unwrap(); - let sh_out = bundle.shielded_outputs(); + let action2 = OutputDescription::read(&mut note_reader).unwrap(); let fvk = k.to_diversifiable_full_viewing_key().to_ivk(SaplingScope::External); let pivk = SaplingPreparedIncomingViewingKey::new(&fvk); - let result = zcash_note_encryption::try_note_decryption(&domain, &pivk, &sh_out[0]); + let result = zcash_note_encryption::try_note_decryption(&domain, &pivk, &action2); match result { Some((n, r, m)) => { let hn = Hnote {note: n.value().inner(), recipient: r.to_bytes().to_vec(), memo: m.as_slice().to_vec() }; @@ -447,41 +445,15 @@ pub extern "C" fn rust_wrapper_tx_parse( let parsed_tx = Transaction::read(&mut tx_reader, Nu5); match parsed_tx { Ok(t) => { - let s_bundle = t.sapling_bundle(); - let o_bundle = t.orchard_bundle(); - match s_bundle { - Some(sb) => { - let s_o = true; - match o_bundle { - Some(ob) => { - let o_a = true; - let x = HrawTx { bytes: tx_bytes, s: s_o, o: o_a}; - marshall_to_haskell_var(&x, out, out_len, RW); - }, - None => { - let o_a = false; - let x = HrawTx { bytes: tx_bytes, s: s_o, o: o_a}; - marshall_to_haskell_var(&x, out, out_len, RW); - } - } - - }, - None => { - let s_o = false; - match o_bundle { - Some(ob) => { - let o_a = true; - let x = HrawTx { bytes: tx_bytes, s: s_o, o: o_a}; - marshall_to_haskell_var(&x, out, out_len, RW); - }, - None => { - let o_a = false; - let x = HrawTx { bytes: tx_bytes, s: s_o, o: o_a}; - marshall_to_haskell_var(&x, out, out_len, RW); - } - } - } + let s_bundle = t.sapling_bundle().unwrap().shielded_outputs(); + let mut s_output = Vec::new(); + for s_each_out in s_bundle.iter() { + let mut out_bytes = Vec::new(); + let _ = s_each_out.write_v4(&mut out_bytes); + s_output.push(out_bytes); } + marshall_to_haskell_var(&s_output, out, out_len, RW); + //TODO: write array of bytes }, Err(_e) => { diff --git a/src/C/Zcash.chs b/src/C/Zcash.chs index 9ce0ea2..67a5d31 100644 --- a/src/C/Zcash.chs +++ b/src/C/Zcash.chs @@ -72,7 +72,7 @@ import ZcashHaskell.Types -> `Bool' #} -{# fun unsafe rust_wrapper_sapling_note_decrypt as rustWrapperSaplingNoteDecode +{# fun unsafe rust_wrapper_sapling_note_decrypt_v2 as rustWrapperSaplingNoteDecode { toBorshVar* `BS.ByteString'& , toBorshVar* `BS.ByteString'& , getVarBuffer `Buffer DecodedNote'& @@ -95,3 +95,9 @@ import ZcashHaskell.Types -> `()' #} +{# fun unsafe rust_wrapper_tx_parse as rustWrapperTxParse + { toBorshVar* `BS.ByteString'& + , getVarBuffer `Buffer [BS.ByteString]'& + } + -> `()' +#} diff --git a/src/ZcashHaskell/Sapling.hs b/src/ZcashHaskell/Sapling.hs index a0a4716..d1646ad 100644 --- a/src/ZcashHaskell/Sapling.hs +++ b/src/ZcashHaskell/Sapling.hs @@ -5,6 +5,7 @@ import C.Zcash , rustWrapperSaplingCheck , rustWrapperSaplingNoteDecode , rustWrapperSaplingVkDecode + , rustWrapperTxParse ) import qualified Data.ByteString as BS import Foreign.Rust.Marshall.Variable (withPureBorshVarBuffer) @@ -22,6 +23,9 @@ isValidSaplingViewingKey = rustWrapperSaplingVkDecode matchSaplingAddress :: BS.ByteString -> BS.ByteString -> Bool matchSaplingAddress = rustWrapperSaplingCheck +getShieldedOutputs :: BS.ByteString -> [BS.ByteString] +getShieldedOutputs tx = withPureBorshVarBuffer $ rustWrapperTxParse tx + -- | Attempt to decode the given raw tx with the given Sapling viewing key decodeSaplingOutput :: BS.ByteString -> BS.ByteString -> Maybe DecodedNote decodeSaplingOutput key out = diff --git a/test/Spec.hs b/test/Spec.hs index ac0a218..831bde4 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -12,6 +12,7 @@ import Test.Hspec import ZcashHaskell.Orchard import ZcashHaskell.Sapling ( decodeSaplingOutput + , getShieldedOutputs , isValidSaplingViewingKey , isValidShieldedAddress , matchSaplingAddress @@ -321,13 +322,16 @@ main = do let rawTx = decodeHexText "050000800a27a726b4d0d6c200000000ff8e210000000001146cc65bd6d252d09b8eb0a8ab0aab6d7a798325aefc1d3032fc6d31373a85a25a3a16b447a698f720ade1bc290a74d85574b5b20515391035a67f8d5883dc65ea3ba4a17b009d6f325d41072b3ce240270959a7ffd040e5f16c697d8148973c62ffe037fc83aded21e4c91722b52520a2395c23e9c1a896f4b0f12d32ae8e31833d9d95adae40f6ecf7aff52af184efd390a4c1aa76b5fb1cab6003b1a8a004016f385926661f56d38273ec2c3df7775210310a65fff5fa9ac5509f0784eefea28bdcc67b0ff69eef930335f3b9768529e2bfe733024492101f642f989de8cbf04dd66638e9317780bce47085079675b772664c8007e96597dba83ea9af22ddf07ff1c212983d4a902914431245357527294e69ea5616e720ef1e9215bbfa33ba108b8d07efff2bad1850525d7725c681761c9b8c844a5548afabf176863de7b4cde3901defc3e83d31086d3c6e6af9a5fcc3cfb38b52ac7de84f91df5e0587f7603773401a62eeef10cd3ccf4d927ef42402c32f32280abbeaac33e73ceda52089820a186e9a1adfea81453998c6bbaa0deb41bc4f94586bfee80bad25fc71abe7c6dd44bcb1a6929a0112c7e4f8fcadb9745bde9422b954f72954c4d22db48719de61f383d620935b647337f73d119d79fd208e1d5a92f0855447df5782cd4764ba91efa65d9e4ebaa34e2eccb7aac93a5b0efe0c7664f3cd9384b3ff706ad3019c907cdcfa084351c9f6a0bfa8c78c91272ca66ac86dd6e1d0d6ba9704ea7dc54f71a053dce91f844c1ca62b5ddfe6b53834f4a816b1b01460810d9b87517659f4915adf4b84783a60ecf3bd71569259f1ff90a91a0b314bd4c77976d7893bf42e5d6ad0f8df95eb6d6c69d41490be8e39b2452df3bebfc297d5b0fc97f081890390fb0727a96898585f0120a7da9a798f2032590553f724d8756c67c5b0d1c0d23301c4ed60fa283994fd712aab17ca6360256fd5aef0ebc48f0256e3eda5894b53981d0d46768aefdc85b48c1525b7f134dce5d4ec2d76c03c821513f1652d9671219d744bdce5e69b9a74ca0c7c837668f0d8ffffffffffff9534b3d594e1609b3bace18608750b35a066c57f85e291d194400cb351430bbbe212abba32be071e747b7310863bd5fd989855a6567a351b288144b6e9f838c6a517db94673246ef0010b65f9c0be8aca654f6f57b83d893663cfd389ab96ce50e8077fe588c16b1b5989c6cc262e6658efb9b88ac800e49e9e5999e2651b8fff28fa77071d63790df155ed8344e2581ac5205b31d4f17bd748fcf60e35a9d6048d23c94c7aca8d4e541fda497aa268df9c173af5877a5da56d8fa2a42166900c734b62e56792f6c8bed48e4f108a817e83d64d6a59e38cfdb55c0f8a89bc7507c89326266f7ac03a3941f448cb879bd792bb116d0be8876c0856a76ddec0f0c02e16f0338626013ee5f6037fc6a3c69fa291204039d04d17c11295ee3024aea8f5d381e9b7eb3f938b6f9182bf4f889f1e53e30f998b1cdd23f45cfaa20aaef058248cc2e1c487fcdf54a4bc22a68a17cb6fa7b2fbf333b99feb84643d321398b675634929602126b2fb40171e514769bf82f18c267ce9cda0c24300caa9a5a361144d3b7b9ab2243ee9811d9b2e72c8bb1d145cdfcf6b29994a969b41c47208f5dba8d6d871e490e9b970afec4d8bca40ba51825cdc78cc7cde6b6f235a4105b1d1b5e2765efd753095ce770f070b02cce3316721b9345680c146c2f428c0bbca90d5a8cd0a1c4c31cbfa8ec165ea9f9c71d2d05e3cf8bae5e779786f179c45a3cd8087d820cae812aded04f8acda9068af80ea834f79f1bd03bfd66f8a19074649a85ce877df1a621a867debb423ec0d19015b326fcf6f143aba34029c1da2fc7b099378a366c38c9609ef6a9d9e175e21b0c1ab94a84e28ee7f1a00e39cb6fb59f44e4567e9f85f8f98158263c52ec433c042397c784edb07c28d2bca036f59090e819157375d610acb1993a4107b48da13a371f5383429baee209b2c0cc150fcef79a042749668ba1f89ad24a8c746142191ed0e8fd63624a331d98d50daa84ccf9043076947cf5115b9f8787acd36000c5e72c8d783b29bb28a3e46036d0a592ce8a158ee5a7ac210be72d3a6185c13645d96a8446021b64043ab8b589a20091c152e7d5a993ba94770eea988e289e1536d0d81dbc7046ca9c6d918446bf970894f073c920006681ccf6d1a3f138519c68eba0296069e42dc60f2bcd0f17c400efe4f4e87de8606606dc4fdf31494df4d454d14a440b1d9db4265c7aa9bc8683c68cb149f2cc826427575e2af82e842199a9cb9fdc7243b3bc12f1a71c37eac5cf88ba830cb95728897fa4c177a290d6b2b3814173262da14db9b4ef39fc54f888a6ffef4221ae672fb03bc78ebef479360a682ddb12ea0369a428a6c2960ff8327e9a2f5e5d98ce1eae748db8f6a4631c789b4d751d6b99c97c149a813998d44a7b57ba06c8bcb8a6c73c6388cdcfeb1346cec8fee7bdebf2a2388d9722183eb2d2e0e183cdd092152ef640880f4514f3c5e836cc3a8249413500630aa8da85f9e3cd92bdadbb69a2bab8d71f0b3ec5832a7ddbddd67b34c33b2e12a0c8468e852e4a8f7df45657e9632088aa7c6c5048a2686019cfec33b27fc88e23759938dd55a5dff589c1c21a37da617609e9d8be37dbf9bd6e84ee160fe10268171d969e4611afe9d3482ed4b132dcdd11ee516f36d512a333da20266fd984caebf4937fdfd18ed07b4a45771cf5c8c16c6b258b289a07d136a22acc766011f366c420bafb8fc1a10e42219bede5a3d1166c525491ab60bbd1f973fd3fb2e94cea888e24d5fb0adce51faeda75d62de70094d4b36d38d03cd824d284fad577c3ead4d98bcc8ceccd18174a889b22380bfcc12656e764ea0b8fe1409971283008ed02cbef89d6f544c62c3b001bfe96723fda9190deecba534d69cfa358036fdaf16127b89f925c52d4e750919ffb7182b6a8ad13d0a8e00e0b906978dd24ee11869c1a63837a80e46e1216e2e273aba07aa5b0d97558db0ba7f9ac4c89403c65f1719394e479311f5cf84746e6be6f1abcac03194aa8bf1735811198b5df90dd6cac345779c185c24beda0101b932048dc4144af664a63acc0c395052882ee1f18bd0ddf13bb583861923bc00ed5ae815b964698ca097eda1c4281e039139fa3091890244f926cc4ab773ca8a35d5263d3bb48fd6ac53a4bb4d7d60b36446dbc714c35b5e13a17c5b0c70f67207839d1f7404604aff63b2fa83a4da7dac92aac96b3f250412f8d04a9e298004313b02edefd076c67d8a1316355777814e2e1ab03690e426b672d32ff65c03c592ecce6a70e34fea2e15b9a6b4fd092d027199caf27e84e25c09380b38a5eb8985355b3259aa1d94be74269b84f953053b02ba3be9df872ae5fb2d893188575bdfe222ba267b5461a0d0be274a7d9e6ee51490d98e4cd97978804c4f0f8e9f4908fd8c102b01080f5a02b7578591e95d60f3f56d8e48514b1ce7ea6894f55a32c8ac8564985d18c6b82f8dcde5b315624e9321bdd49dd350c87907cc373c0238a79321e6250e38a0ceb2c060ecee6708c11cb30a49687da9923bcdf011f9aca27e6eb5a8477a2bae2dcff9884cc2349b51a66b5179ed2d8f69e4bbba74c694194e83d04a8566228227eb732a95180c6788483d1f259d52c52fe43357656d50a1cf2902c3124d60d15fc85f0447a1203f824c1106452cfec1c92b18de003f82a0000000000001cbd27436a221a53d08c4838831d1bc60ff7e93df41a51412ef6096eec98bb28fd601c53a5371b23a497062635b5cdde715c23840d37f1cf328f0a2ba96260357689ae3f84a80dbdca1520df68513be1285177d3c0da664c64944de78d8b8d5864f5ac15444cd3204adc4fe487503066c18fbbef8d0515248b0a97577f5aea1d255788ed4bb66d4d56303efe135063392c312b4671963daa20e0ade262984e11263a1588eba3cf829e6131ab506e6a850aacce603e8ecfd6e794c90a772603d80fd2aad6027b34854072a0d23079252adb1ba637bbc650ed4afd35d977e1498d998020bc1c814718b48ba7378a92c56827d3c2f20daa231fa51f0a9188520e2a11149e162489f0d6dbd27cf94fd5775311d3dfbcfeb431bafc3515bbb8c4ba4488c320dca0dfec548fe9f46d8810b3f6b16bb3e3eb0ea130747d3d127c5953ca8d561f8d425a35dc3f2cd831743139fbdcada42308b524313782e23b32d5d54a265eae408623e3b2779fe60e13cf47d54dfe520f9f4e57c68aed31f78629a9074d72ab87bea993a38f95ab40df3ef01735e7d44ad365a786e0d3032f1c1dc4e6839c974185dbe63f8725e79831ebe269f94c96705639ab38d5d0700da04c6a9f686e1ea13391885287ba43cf3ccef1c2227918f15ed55441c45adca84153530bbfea3cf37adbf84831a2bfcbf0ca4a4bbd90e623789fe993dc17503ec11b1ef3049f27b27ff778af364d634a46165cda1dd8241cb88740bce74a73e7e3d656df2dee05bb561a85e64671b191ec802c5bfaca49b8168e44271cf13df756395896ff41a99654f55b6951f20d04b2007938a420218db8e37445ef3267130e288e3270b13a92596a26043e1ae84f3934cdb13363bc2843f74a0f6608a36b52c985132aa427c56b7275a864b3c76502c37b8abb8d0286b3199c78492ba8103f5a23c6cdca2292c75d7d6d7080108850807f78af3dc7e418371c6b8951bd89b79fa586af4e16096b08ac1f4dc2b1e4feaa5c040bb002b57311523197b6e2bef5b79ac9c9b4a339be6f6bf7fbe9b5c93862c87be6647949c70bb2c7e268e2ab39cbadd69de628376b3af744eeabc85b599bbdd09defacefa443e05c9b5f259a7783743fecf1a749c57cacc85703269ed67db1d8d475f6fe25d66f84a77379411ba123d98fcb3ae4eec306489a08372893616a91268ea6bf34ddbf0fdef1360ab9e82f4ac80a24e41f439af06fadc223c61f445b7261eda5e1320e269d1277631ee2245cf930244bf8c04050c514e2d59035b80827586cbfeb7da7a59c1208aa86390b9dc7a9b6ef38879ba4deea5eef47c5c98d9167594cd730abdfaf082090efe759d1b13199d739c112ae324ba24b275bf1d89867b81f4580a7ea3a8d3d07b45e2de6c1c7099de3606873b13f3083ecd1e84456c9a1b1d358075c68b1a7cf0b1f26031a2909e226f5da7877d0085b879165ec4b1d9abb7b0732ab4a6f22d9a7bbd0d494ef3f9af4903dc733fe92c6b2f557d1406d223a93e8ad6e579ebcde9c39a5652ad31335df924e5b6a09a0191821b4a0c8f886e2d7860b75ae79ad9dfbebf3500c8b9762dcd131eb5c8b866b5efb4fbfdcc5e31605c2b7d2ff8db5198a6c41bcf880065ff232ff8f84ca3f8022d3428359dc9fb19f57a6ad3f3d174d8a348879a754b37095f01d9a7f6f873798b97dfc5d7c7eaf0383b3fdccdcc11b30dbb3a0fe3186a36c4ddc9674624e38a81cca60a9bbc1b124021b61a383b7547d6af187022c133ba9d6dadf711a3af3b0255b859b214ef6c5dec592248fc94339a64f19196ca0fdad80f7f8e3d78b1f783b1f038008d0d106bd86e23e33ae5728872d42a555bb36d0e3303f0b4ab41180f4251590ee3ee244b77191c31b9f3f990f71c6e237b9dbcf7ca21c9b4c2446b856c67861785bb9edb920b8f530a7a088313ef044419a879f26db137e1557d079315844f9f60bae03d8cffa7a28bd2857a001fd5d2d999fca95ff91df0e228567f6c9ff592b77b7ccdc93a951f7e34910361a8f4fb517e1c9fb956a3bb50ddf37ed37e8d26adfc0f71e059ba95ec77a1e34e1b3420c893f89b79fc72e3a1d864fd35526727f939badc29740f5ef9c0bb9a3f72e1e08b2ef2ab366f80d8e14e03e92162736e2ccb7cde82b2af08de8a6a81c03c63e2396974ec29fb122d818a2d2d5b29d11b704d3ac3b39431099b7b2f6aae04d28a2182b55380503127e4986ee9e8d5c0c2058e09e4592d08d013a4f088e45403720160622236bd56fbd9cc240efceb1b23c19ceafec49e9d5776ca9da5f7810ac979cf6ec5f678c09257abf79c9ab55dea00054e11b62c0a0ed4363d0a96a37ae1a323aa93bb1af253885afb684ed30caf5cf3b37afe6a6463a16f34cc28b4530c6bc6281f597bea476cd9a773205b96d47ed4b0bfeeee39b7ea44ff194911bcddbb161c2c0ce9488978b99e880d8e43624dbb4a567483ed293348d752634b2f46219575175e3c249b8e4e853142b66491aa1c142e7bb558955747cf2bd61ac802a2a4784d9fe080f771dd537d0ff928b3b04029d9ac03175c2d535ad7f3c123eff30c0437b32dd9fa31b2976e369b89b79a2a95e31aee15a462c5fe25fd937ce6b0795808d16163f2cea8f19b7c83913cb4a793576aa5c0ddfb6415326d8f2be0621017616c85ee46aa768b077dbec72311cb8a0f78ba0621a277a79af6607063839a52c6825a20e1c5818b24628862aff5fbbfe87311866b9956eeea7412ef69a3e4da84699b8d8b45c74ac96c3356989fd4962ba79fc26a92488304fd9f42486266b433eeb57368d26ae91ba4e7ed812581f790314cc7f44639aa7f6775618111369e8e2d68a6ab24388824bbbe3c3b0fbfb88635c1fc3216af1af40eba3555c0b0390a18ccba9e68afbcd21ecb212aaf82846f0a55793945902c48cac5d19332e23248a464529f4cca177137c508b6b13637e7df523254f24b8343d19164174202bcb00d5fe618b760c374f69b5065b1f9acf91af95abd7eb271586cae14fc835f633aacb4cd2ecd0f0ac08b688ef4d13b8a7b4c487ad46485bde0e340309672dc38af275e6ab525971409a39eff0ad134b1674db5d1f9725874e36d8730dc034b0a596c6e0a26e521c199d3e3f86815a64d148ffc394290b19f15390934b5d0da27dc8365360511628b93ebc375d2a531e4f4cfa031eeb501afe96d201c7b6078bbceb8e5d8615599f4c613bbb81a88f4eeaa57a9c008125073a30154044c422eaf2a32cecb15aab0774bb44e52b1792d154b8036ff9224af53e023fc011ab251d47d7d76e55c5015db1926d43c56d055feca1259de53ce98a2faecb5843ce17a3e83ccf678f5d13a8321f278a670c684e62b720e1eefd0abb2e9d0a3ddea81d5eb6e380a1c22af5587daac852e93a86f5e4293c18bb26b32035c7e5ca20cd2e3eabd3de0e55092a79a42c7ccc0aef033d6683043c2a29531a2ef1f503595c0e464ac042153c4685b062275f88bab93cc017f1ef9dc6f8aeab7b0b234f1c543420324512554f1786c82b37836238a4dfdf86f97d09ae466eb39d5f3ab159e2060be309d1284be133ab40abd61468c1706f7f9e57a7bc747479693a03edc8863dd196fd7cb2721260e42f4f606389c4972c74d357e7467b61cb7f455562015d29a59c7cafe0df03f26b77bca81c2bbac8cbccf8a65190b0c4e5ca832e82ce4e11044433aa397106cafc05634ad778270d20d8a13068586bc6b582ea24524fd921a5ee22dbae5296ee86d80f12b78bba26f8b42c1401b75d5cfcc4775c5cd1cb0a9248dabc8f82d216787b2a2780f7d13a5ee8c6ab56399a8dd5db3a152677d01eb8ee98d1927ca5069e0d1ba3907971a2199ba3634b48e570dd97d93729a6c43e4f359e2d89795218d52270a338a1f511b1f008cd04553c1a89caf987fd18c329be7ac2282084ef1789615d7eb7afd2261f606d3953b8863abe57796289e7761b01c3ca0faf2291287f9ad7027d7f0876b5f77d2a7f87dfa6ad4db905d4bcad042f403824aee3b4f8d7b5ab027fe1eda9d683db24f56f694b0b10ec72ba0df40bdd6e52b4a7b8d064ad46c7490c4705c14b06ef55222435d2d6316c7dfee83d225eaa431c11a4b85b0bafbe66fda1abadaec8eabcc2f8c688a7b9cb2942597f20cfafd7520892c535bbf6359a6989a84dd89989d95b8e5222c3aca2e8b0f8881d759e450466b75b5f36b7b723b0a212edf52abd591e7e545a3974b8b31b84b523af7b47e3804b5a268c86ae0bd7c80bc6b578b79f749eccdb4a00813925ef40259ac10bbd0fc4f2fc536c30f7c1efe68a52bee22f57021d23f445211b36fee6302202c9b62c6cf9064a2df424563f9f805e51c4092482253e4c258d53b80a2d26eec9fcdc104f7124d876a3ad573a7f419b0d67a41a34dac9d8f28cf9519b9c2677c9d1e720667d5fce26091d64c6df8b46c98b58017de0d055651e8caa3a230f57aef214ae2f27fa85400e34ce7087538fb6b854a6ad534780052210b8b8c90b4de4c2afbad9f58a71770ec186cfa44b61b53876bf904972078673845ec3181caaccf11f71a8e2a502deaf144f16df1df3bd81277dfbf6e1ae5a17363725ec31759b743066fbd4cafb5eda6e09418bc375f42e0dcbd4624dbe26c5fabb77152124f400e677004fcf3c862a9b5576140cbeb800fcde4409caae06286cb643842687bc6b89738374c7c759c911d7bbec8613f1fdd996d4b970fb6f2a9290a84c34d5fea6b8006357c8e6e9d4048d5a8ec476dab0b55e8358ecf7d27c31da86681f3fa74d072b1150223eaa21c4027378a99c8a2dbdc07d4062c401e92eddfff82841292bda2798c4e2ee9e09f618cc181c91a4dbefa44c410dff5cb705ee005e3a0470c13baddf9066109797a3b51e73a0ef229796d330aee0c0160529a4ca3b39e861ee5c4a0f78619007ecdf32266c7c42f0c972b91cccc45f6f8688b2692f298721cb5da39cbca9a5adef6969a2592ba421680241f8a5384bb92e70acf79c2f41d1171dfc6e1939887b9c8ea94429bcbf3532919fdcfcd0f443d3c95515b41e3f9c84bdcd3de1fd481f98482f667f2d017e3579208341e9a225f85516c8ca133cbada77598b6f596e6151eca377fcc8029cc99a879b26d975684173c0874509117ece4136bd2d69848f858c05e8ba3421499d7fb5e3e7645fc135117d8fdd1dc46bebaedfbad4dc7cc23fad6e696fe349712cf7579b4e63b38cc7d02a4c6a33ee4117d7ccdb86ea02cd791756b2a3c516d59d39ed83a8c328823f1934731820c187624219b487ca86edcc2f61a064e4e8d17f58f4a71462f3f0cfef6be95c0eb3737616de5954096d761a51534b36d798c651541acfe2e5bed6e58c45c46e014923a342409d49e782054f2a4877332e0adb0663dce0e84df0ff0d71c4c5ef18d9cacc8b8d47c78d53fc7649bab719334601c79a345d2101e65d4c3f1616741b24fdd9f7d6569fb8de67799648b323856c2ca96ac91823aa12249934c7050505258c43763ac9b174d55fee71c7817d65056c30d7853500656040621eff9f291e9db198772451cd3d58bc9421f6191a863777edf49125543b22ba127ea3063e1989bc4635d50bb22270948ba594b2066d91e1589aa0c1f476af8b170820ae0d2409df83819579621161ba55cc8d020e1ea68cedbccc9a737746d824b06e12cd2e3730260fdc52a68b6e142bdb997d8f93f25d241360285e7372939828fd54c015ba90bb4d553558ffa2558a43098644357860785addc455bf1ef4ffa5aebb8f002eb0f6a8ddd6f45edcc7cfe88d7ae7141b9113a4ab851b5fa8ff39c7024d2b3202841266f256b1f4ce4e4ea8b83d0067555ddd56fb1f8ed6cb5ce7340cdc403fac1dd3b48c373629202c70f1f95c0001b4f94a9b4bad9e1c64429b541ff57a2f2ea0c1fb2af8f0e490fac415cc3bcdd5739f2069e2bfb873206b8c8811619f1db39386cd2dd748c6301239b4e873f3d4618a43c52ef7d8abc0ca127881dc8809dc20a8730718c64c5a132cf638ec1e3b3cedaae55763637b4c6341baeff17e14c1e3c34e5d23befdddec0c7e25f0ebfa90a0bca07a0f5446507b7fb9cf91cb3cb8d24a111f46e7dbe3ea62b384eebecaf8de49b15b17f9cf151c4c3d33220b3adcf9f87222813ff2125120e77c638ab19c1daf9631d53b3efee67c3c40c9c23495848d4c5ecafc0998bf2704bb78ade666f414e9c6bb100b90451d93397d062b741a3ef20e7e6ebebab8238e2adf415775179c866eb4a2f628924674b20f711d2515d3b024e7e0fd3af6e116cfb0030709fb9bed4a441646c03dfa0b11cb460415ccb3c0999b738a0b07d55be741f3d2646a3d9e22bbfeb46b87acb6fcc2d83cdf44b656d2269a0734259a738d83a7429bfc99da7238b9497fbb10e0f623dc1f793e3be2326a7e55677e20ebb152f71794fd70d7698d0cff1334341b3a8eee38d66a021cae6f8dfe67913fdeb8d46efd9b77f28950aee7ce112f8f0dfc04132a99d95dfa923d391e06a578056ec8d15ddfb0fe8cd2790ed55f8254c6351d471c7273c2e61c2694bf6b0a1173cdc2d422246ca5cc51a9b8669dc331fe33bfe330d5167a244cf041f8444775562eaa8ddb92e0623d4c689db0919ece87a7feeb1ddd2d034a0d884d6bc8b5fa5fc7df00cf667dfc82c9e98e9b57fbe07fed391729034c467b46d3d16ed72c8ded069a117ca2d28e71ef4c37f1b1939590af19433ff2a784ff38134aacc59719673c53ed047d79e0c36f3acc03f64458826107853ff8d02de9362ffada79feabc597d019de6afb6577dbbefcaeaee801a49b1718151690b43a4d70b2fcb73ece3e3ea5fe2a9616cc6a9a494b8c6c7121497688652eb854e7bae19af3e5ae1a8df75138ce18621d11f15054ac8971f6ac084bd90e3909b46db321916c9e3c8950f3cc8e0a10dd255bd6b359c73dfde7de6e01538d03609253968177af14d327337bc1bc4e866c75ab5b69125f9e9a758f061418110a615b97f733f899382e2a14fa0ff0d1bda3b7449e1f54362697aef064fc9dd50692ad2b9ec286a1b7c1f845b09f4b1ccd46142f9e272d018eabf76c23c26b1831762a8a2c9152ef0cc843ebba20bf979e851e76141f41e817cca0ac60c1baf239a6c920a244b0be031a3b65d76eebae15bd9d49c53dd3e4303ca379f26840901f1da886d330d8ab4408a25eaa280ddb545b968b4aa59ca6f9cecbf82984264a3232b97498be0729f3d97dbd64823393b42794ec1676244212af382c37d53d66019a5db34c3d8ac156cac852ecb1c57f0f6057815460eb9f666c368acca6dfee0d2b7bdacd2148d74cea3bbac8b528f00809f403b1964b9d199d2db93363a53732efd97c66f28cb51442dbfb1c912dce078944e02889f9395ff2655e2d7118a11848e3ca64efb5eca119240b537b5d4548a744d96962e9b2a99af73bc4d753df16fe75a08005da7773b7bef68a44f56951a3a93995c5d0516a8b2c39abd2057b3d86ba7fe511dc33292596fcc4d3c67df88812e26d3628527752a81c8ce6eba711ae7ea8748229caa2580e18937f45cbfb0041f0a9254eade41c95ef4f390db6174e6b0ba89af344c215393b627348ebf00901e343c1599c832bd2cfb751750808719f18119ab3fee6baf5fd4b8bf7c92a271ff24576984610abaf1b2297c18a2784f57d7aad26f52c0d60fa6cd2484492a1524dd14924eebf53348ee2dca0643ef47381541b91d1843817067a94bb7c79e4e08c4d81c7266afefeda4a0db21b0db629cedf13b4f6ba11e6480116a7c9780009ee0e1e2bd543dbb68bea356a6010a1e3ded4229c4d8035420833fc83337dce4bc483eec76f479f33be2d6a8fb013813952eb66bc247f37f9def7e9bea0f277f380079aeb48c192b9c2c24f909435586bcb994ae6f75eea111c8842b855f90df21e956f839aef89bb1b96899e343cc66814ac08eed4defe5e2a62c54cf83a27f0499224e02bac652aebe6529143372c483edcdbfbdd5a02106fd3651875f23724d2154c0138ace745d038d6cf6b233ae2bbb8313c8ab59dc896844da5a6610845d9147e57518ada61b7debcd66111d609587632cccb936751d099639735f09f77b2b9aa1177e95ac0f8070bcbeb01bbfb3e7a638811d919016ed628865e2405aa17f5129ea43a836a76ec7cbd943e20b1e388d47db2df3394785e27dd7efbe350ccec6057af7483303a6a588d4005c05ff7f3c90dc5b9a0f95c3c541ea40615f6b0ed6cd3787e608e7612b52199062991a63511b3055dfab25b18ab55948d9d529aff91a0d8aa7705afc187e32d1107c07623a309390f32c62e1abb830bb090a7dc4c6817b554720fce354563662d78cc2bcd0483149b304ab86502f0ad36ba2c262fa60b1b84be2499e56fd72129c0fdf1961214df8dd6c9b831109fadc7464df090aac96d221ab387b6eee3080ea0d94e195ca795b5d80c42946c0c353e39075bb1aa4f3fac36a77445cd57b11b3c7ef0e89aca911be35fbb72e1e2fff84b7ece66bb8b3dea52b0aff473475d298f730e79ff77e955856817514908baaec1b53f39f9d5dcbb1cef29536a483a1845d8538e5ec43f634914bae4f2319579adcf3f0fbb151c80b54f02be54be612f9417f916579863dc16da5af07fab05db2094c77612b445b7b9784198abbfc8387486449969118d3a83142022017d69b939d30fd6b1c5ba360c0e37f264b5d604c7ebdaf327874b22d4092dcf35aad93f0f0b3037f90000d4a1892dab827d3c2f1ddede948d330706ef17bf442f8228c168401541ea89a837f81cfffde8480e1b6014f2f5d3132ee740e04622600ad93dba41e39267f17554b29102390f8196094de19e8d966d3c03a9e3348494b54045629224612734a1304ed76d17a465c305675bf99b7aff1950dd2012e6cbe8d15a9e35a7c16081bf647e2766c7b8e9d12ef8a2c94fd7531f6d1a7ac7ce6bac028917a2a71c8eded4f3ce3be8c7d98422094e88414338fc25839a582b5965c71179ce3ddf7a645bb163f3265f4bc4ebe91ff25d86520696d237741ba73084070e23494d6a890e1ae748091a6936a3137d16dd14571f86ee118bcb17c8ecd2ed20c25cbab79a78c53fba1d5db4fdb15e6a124bdb2d41fef080e7318f78c67451beff04b58a182f592fec3feab6011be4663dca8b62ad2a8e40c716b3c8a170e1574517c44e146109d223529b0de480aa168b5a4b0522cea03dd26a684b08c58aa2e1fe41f44ba8837737a74a4681b0508d9604aa7484c5ec68ccb369c056702b03f75722033597ce5b55e5a88f14e3ee23b1c96f5d38604128cf2e901a745996f4a29d412e5ce1b4ee82cf2f248aa80f8c8f2eb7b6951f1b0910857a17edc319bc76b1a59fe05f9394a5b16a91ef15afba9ee2e115c9c9afc51a8d7c4a61cb335724855007f564fec341342d53717023118917304c2dfcf0bd79e456c94718b8388767564d6055aad1d505f36506fd6e8cb1a10f6fe8a67e772030dab78b43ec15fae0322fe991944be86cd0bd78ba00110474dec84dd9597a602c4a6c97a27f13159ff1e32fa63e28f63278b09c9d051cabf8bb49f2780d4c33ca061ff16c6288ff29d46a22e31c719dfaf6087590ea8197c066f9057e0c24b40f5eb205b730985daaf7e7d85fecd7449677c0ec1abd7111d795de6de4a6ab30cf266987dd55569f2a705ae16f51de738fcc4cc218f2cb9dab8af509ba26ed93ddc7a73028d9187527fa6d554b491faade05852eb50847affb81b58d2501989d5b4a9de4237b33bea154c4d016b08f3e1c39ee61382fb500be2123ccfcd30e37c88979a19d636c9dcf5fcec822e5558cb2939e40e1af039aa942b503c9fce95f8ff51a2977f58e4d5032f9099fb4783b7764dd1299a1a89f21c1f5e38038f219e9a4e9d6c6edaef70132ee1f2a580227b256c9139d339183e278277a9dd260bf7d8a9853c4185f9b6509ab9b67a3563ed0b9afca6f2db28258025d8c2036cc4cd9fb4f7007dca96597c26476def29191cb0c77c44df80994ad3c79833820ce2f1108d17a083fa857397d2839c37899ce2e634578b3eee0135f66cfe1aeb7bcb8732fedba16093a6e666321236f56e285058d00c1bba2db3263844e4b0ea013e7a8d370ecae9ac7a9620eaaa244b25be1eb6e71368f550736d3ca13747edfb6254c9a373aadcc30a148907d39e81bdda8a53e8644b39a1625de851fc5d759686c63c76bb162b244415175eb0ccaf3a63d4ad910369e30313bdeb175ace63ad33e400e642dbdcb472a336d82d2f818e6888e5aef472d03fd298a5d14b8da432b72e6c5f5694887a7e4e165bc41055379bd204298d65796d5351a9121f949904bd6c87510649127ef5797d432c6c759939c99026d57076db225d7d0df01ab9b9e3e1221e7e323ff752eb7988cd2b2bbfc8ae7fdd23c3f7cfd2ae2679014e986e3c01b6c66a67fbf69483ad3e89c1f2dd0867f411151e048d81212c0b5e2fbba9d317f283f97c9576180121f41856355b906a22c69869bc27729db5974c5b1257b35d186dcb4e9f7bc51dc331c0ff9baed5955e5052a6ac7743b" + let x = getShieldedOutputs rawTx + describe "extract Shielded Output bytes" $ do + it "should have outputs" $ do null x `shouldBe` False describe "succeeds with correct key" $ do - let a = decodeSaplingOutput (bytes rawKey) rawTx + let a = decodeSaplingOutput (bytes rawKey) (head x) it "amount should match" $ do maybe 0 a_value a `shouldBe` 10000 it "memo should match" $ do maybe "" a_memo a `shouldBe` "Tx with Sapling and Orchard" describe "fails with incorrect key" $ do - let a = decodeSaplingOutput (bytes badKey) rawTx + let a = decodeSaplingOutput (bytes badKey) (head x) it "amount should not match" $ do maybe 0 a_value a `shouldNotBe` 10000 it "memo should not match" $ do maybe "" a_memo a `shouldNotBe` "Tx with Sapling and Orchard" @@ -371,8 +375,8 @@ main = do "98e72813aeb6ea05347798e35379bc881d9cf2b37d38850496ee956fbecd8eab") (decodeHexText "cb9926f519041343c957a74f2f67900ed3d250c4dbcd26b9e2addd5247b841a9fde2219d2ef8c9ae8145fecc7792ca6770830c58c95648087f3c8a0a69369402") - let decryptedNote = decryptOrchardAction a =<< res - let decryptedNote2 = decryptOrchardAction b =<< res + let decryptedNote = (`decryptOrchardAction` a) =<< res + let decryptedNote2 = (`decryptOrchardAction` b) =<< res describe "First action (sender)" $ do it "Decryption fails " $ do decryptedNote `shouldBe` Nothing describe "Second action (recipient)" $ do -- 2.34.1 From d78c269d96fe7d8a626cf701b8051c40f251e232 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Wed, 27 Sep 2023 11:18:00 -0500 Subject: [PATCH 10/17] Update raw Tx parsing to extract shielded outputs --- CHANGELOG.md | 2 ++ src/ZcashHaskell/Sapling.hs | 30 ++++++++++++++++++++++++++---- src/ZcashHaskell/Types.hs | 12 +----------- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f279786..f3ad8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `makeZcashCall` function moved into this library +- `RpcResponse`, `RpcCall` types moved into this library - Functions to decode Sapling transactions - Tests for Sapling decoding - Type for block response diff --git a/src/ZcashHaskell/Sapling.hs b/src/ZcashHaskell/Sapling.hs index d1646ad..ffec5f8 100644 --- a/src/ZcashHaskell/Sapling.hs +++ b/src/ZcashHaskell/Sapling.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE OverloadedStrings #-} + module ZcashHaskell.Sapling where import C.Zcash @@ -7,14 +9,23 @@ import C.Zcash , rustWrapperSaplingVkDecode , rustWrapperTxParse ) +import Data.Aeson import qualified Data.ByteString as BS import Foreign.Rust.Marshall.Variable (withPureBorshVarBuffer) -import ZcashHaskell.Types (DecodedNote(..), ShieldedOutput(..)) +import ZcashHaskell.Types + ( DecodedNote(..) + , RawTxResponse(..) + , ShieldedOutput(..) + , decodeHexText + ) -- | Check if given bytesting is a valid encoded shielded address isValidShieldedAddress :: BS.ByteString -> Bool isValidShieldedAddress = rustWrapperIsShielded +getShieldedOutputs :: BS.ByteString -> [BS.ByteString] +getShieldedOutputs t = withPureBorshVarBuffer $ rustWrapperTxParse t + -- | Check if given bytestring is a valid Sapling viewing key isValidSaplingViewingKey :: BS.ByteString -> Bool isValidSaplingViewingKey = rustWrapperSaplingVkDecode @@ -23,9 +34,6 @@ isValidSaplingViewingKey = rustWrapperSaplingVkDecode matchSaplingAddress :: BS.ByteString -> BS.ByteString -> Bool matchSaplingAddress = rustWrapperSaplingCheck -getShieldedOutputs :: BS.ByteString -> [BS.ByteString] -getShieldedOutputs tx = withPureBorshVarBuffer $ rustWrapperTxParse tx - -- | Attempt to decode the given raw tx with the given Sapling viewing key decodeSaplingOutput :: BS.ByteString -> BS.ByteString -> Maybe DecodedNote decodeSaplingOutput key out = @@ -35,3 +43,17 @@ decodeSaplingOutput key out = where decodedAction = withPureBorshVarBuffer $ rustWrapperSaplingNoteDecode key out + +instance FromJSON RawTxResponse where + parseJSON = + withObject "RawTxResponse" $ \obj -> do + i <- obj .: "txid" + o <- obj .: "orchard" + h <- obj .: "hex" + a <- o .: "actions" + pure $ + RawTxResponse + i + (decodeHexText h) + (getShieldedOutputs (decodeHexText h)) + a diff --git a/src/ZcashHaskell/Types.hs b/src/ZcashHaskell/Types.hs index 1c5d680..51fe2c0 100644 --- a/src/ZcashHaskell/Types.hs +++ b/src/ZcashHaskell/Types.hs @@ -103,20 +103,10 @@ instance FromJSON BlockResponse where data RawTxResponse = RawTxResponse { rt_id :: T.Text , rt_hex :: BS.ByteString - , rt_shieldedOutputs :: [ShieldedOutput] + , rt_shieldedOutputs :: [BS.ByteString] , rt_orchardActions :: [OrchardAction] } deriving (Prelude.Show, Eq) -instance FromJSON RawTxResponse where - parseJSON = - withObject "RawTxResponse" $ \obj -> do - i <- obj .: "txid" - s <- obj .: "vShieldedOutput" - o <- obj .: "orchard" - h <- obj .: "hex" - a <- o .: "actions" - pure $ RawTxResponse i (decodeHexText h) s a - -- * Sapling -- | Type to represent a Sapling Shielded Output as provided by the @getrawtransaction@ RPC method of @zcashd@. data ShieldedOutput = ShieldedOutput -- 2.34.1 From cbbbaa0fd0af4c7fc430e1d98c843cd519faa0c5 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Thu, 28 Sep 2023 13:56:31 -0500 Subject: [PATCH 11/17] Correct JSON parser for raw Tx --- src/ZcashHaskell/Sapling.hs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/ZcashHaskell/Sapling.hs b/src/ZcashHaskell/Sapling.hs index ffec5f8..f5a7c43 100644 --- a/src/ZcashHaskell/Sapling.hs +++ b/src/ZcashHaskell/Sapling.hs @@ -48,12 +48,21 @@ instance FromJSON RawTxResponse where parseJSON = withObject "RawTxResponse" $ \obj -> do i <- obj .: "txid" - o <- obj .: "orchard" + o <- obj .:? "orchard" h <- obj .: "hex" - a <- o .: "actions" - pure $ - RawTxResponse - i - (decodeHexText h) - (getShieldedOutputs (decodeHexText h)) - a + case o of + Nothing -> + pure $ + RawTxResponse + i + (decodeHexText h) + (getShieldedOutputs (decodeHexText h)) + [] + Just o' -> do + a <- o' .: "actions" + pure $ + RawTxResponse + i + (decodeHexText h) + (getShieldedOutputs (decodeHexText h)) + a -- 2.34.1 From a6a69ae4cc83f18228c20da6c1b34151c6ebd36e Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Thu, 28 Sep 2023 14:23:42 -0500 Subject: [PATCH 12/17] Integrate Bech32 decoding into Sapling key validation --- src/ZcashHaskell/Sapling.hs | 9 ++++++++- test/Spec.hs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ZcashHaskell/Sapling.hs b/src/ZcashHaskell/Sapling.hs index f5a7c43..ac583cd 100644 --- a/src/ZcashHaskell/Sapling.hs +++ b/src/ZcashHaskell/Sapling.hs @@ -14,10 +14,12 @@ import qualified Data.ByteString as BS import Foreign.Rust.Marshall.Variable (withPureBorshVarBuffer) import ZcashHaskell.Types ( DecodedNote(..) + , RawData(..) , RawTxResponse(..) , ShieldedOutput(..) , decodeHexText ) +import ZcashHaskell.Utils (decodeBech32) -- | Check if given bytesting is a valid encoded shielded address isValidShieldedAddress :: BS.ByteString -> Bool @@ -28,7 +30,12 @@ getShieldedOutputs t = withPureBorshVarBuffer $ rustWrapperTxParse t -- | Check if given bytestring is a valid Sapling viewing key isValidSaplingViewingKey :: BS.ByteString -> Bool -isValidSaplingViewingKey = rustWrapperSaplingVkDecode +isValidSaplingViewingKey k = + case hrp decodedKey of + "zxviews" -> rustWrapperSaplingVkDecode $ bytes decodedKey + _ -> False + where + decodedKey = decodeBech32 k -- | Check if the given bytestring for the Sapling viewing key matches the second bytestring for the address matchSaplingAddress :: BS.ByteString -> BS.ByteString -> Bool diff --git a/test/Spec.hs b/test/Spec.hs index 831bde4..660d342 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -279,7 +279,7 @@ main = do let rawSa' = decodeBech32 sa' it "is mainnet" $ do hrp rawKey `shouldBe` "zxviews" it "is valid Sapling extended full viewing key" $ do - isValidSaplingViewingKey (bytes rawKey) `shouldBe` True + isValidSaplingViewingKey vk `shouldBe` True it "matches the right Sapling address" $ do matchSaplingAddress (bytes rawKey) (bytes rawSa) `shouldBe` True it "doesn't match the wrong Sapling address" $ do -- 2.34.1 From 697ce83f7c3db28e691ae0924c4857511aa96ac7 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Thu, 28 Sep 2023 14:50:53 -0500 Subject: [PATCH 13/17] Correct Sapling outputs parsing --- librustzcash-wrapper/src/lib.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/librustzcash-wrapper/src/lib.rs b/librustzcash-wrapper/src/lib.rs index cf4f370..744a911 100644 --- a/librustzcash-wrapper/src/lib.rs +++ b/librustzcash-wrapper/src/lib.rs @@ -445,15 +445,22 @@ pub extern "C" fn rust_wrapper_tx_parse( let parsed_tx = Transaction::read(&mut tx_reader, Nu5); match parsed_tx { Ok(t) => { - let s_bundle = t.sapling_bundle().unwrap().shielded_outputs(); - let mut s_output = Vec::new(); - for s_each_out in s_bundle.iter() { - let mut out_bytes = Vec::new(); - let _ = s_each_out.write_v4(&mut out_bytes); - s_output.push(out_bytes); + let s_bundle = t.sapling_bundle(); + match s_bundle { + Some(b) => { + let mut s_output = Vec::new(); + for s_each_out in b.shielded_outputs().iter() { + let mut out_bytes = Vec::new(); + let _ = s_each_out.write_v4(&mut out_bytes); + s_output.push(out_bytes); + } + marshall_to_haskell_var(&s_output, out, out_len, RW); + }, + None => { + let z = HrawTx { bytes: vec![0], s: false, o: false}; + marshall_to_haskell_var(&z, out, out_len, RW); + } } - marshall_to_haskell_var(&s_output, out, out_len, RW); - //TODO: write array of bytes }, Err(_e) => { -- 2.34.1 From 31579a6bb23f4c7473c528f6f377ac5ba71f2905 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Fri, 29 Sep 2023 09:02:05 -0500 Subject: [PATCH 14/17] Correct return types for Rust tx parsing --- librustzcash-wrapper/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/librustzcash-wrapper/src/lib.rs b/librustzcash-wrapper/src/lib.rs index 744a911..e5879ef 100644 --- a/librustzcash-wrapper/src/lib.rs +++ b/librustzcash-wrapper/src/lib.rs @@ -457,14 +457,16 @@ pub extern "C" fn rust_wrapper_tx_parse( marshall_to_haskell_var(&s_output, out, out_len, RW); }, None => { - let z = HrawTx { bytes: vec![0], s: false, o: false}; + let mut z = Vec::new(); + z.push(vec![0]); marshall_to_haskell_var(&z, out, out_len, RW); } } }, Err(_e) => { - let y = HrawTx { bytes: vec![0], s: false, o: false}; + let mut y = Vec::new(); + y.push(vec![0]); marshall_to_haskell_var(&y, out, out_len, RW); } } -- 2.34.1 From 00090dbfcd511895c2d6b9cced6d55545c4d4db7 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Fri, 29 Sep 2023 14:06:56 -0500 Subject: [PATCH 15/17] Improve error handling in Sapling decode --- librustzcash-wrapper/src/lib.rs | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/librustzcash-wrapper/src/lib.rs b/librustzcash-wrapper/src/lib.rs index e5879ef..f372a2e 100644 --- a/librustzcash-wrapper/src/lib.rs +++ b/librustzcash-wrapper/src/lib.rs @@ -364,16 +364,24 @@ pub extern "C" fn rust_wrapper_sapling_note_decrypt_v2( match svk { Ok(k) => { let domain = SaplingDomain::for_height(MainNetwork, BlockHeight::from_u32(2000000)); - let action2 = OutputDescription::read(&mut note_reader).unwrap(); - let fvk = k.to_diversifiable_full_viewing_key().to_ivk(SaplingScope::External); - let pivk = SaplingPreparedIncomingViewingKey::new(&fvk); - let result = zcash_note_encryption::try_note_decryption(&domain, &pivk, &action2); - match result { - Some((n, r, m)) => { - let hn = Hnote {note: n.value().inner(), recipient: r.to_bytes().to_vec(), memo: m.as_slice().to_vec() }; - marshall_to_haskell_var(&hn, out, out_len, RW); - } - None => { + let action2 = OutputDescription::read(&mut note_reader); + match action2 { + Ok(action3) => { + let fvk = k.to_diversifiable_full_viewing_key().to_ivk(SaplingScope::External); + let pivk = SaplingPreparedIncomingViewingKey::new(&fvk); + let result = zcash_note_encryption::try_note_decryption(&domain, &pivk, &action3); + match result { + Some((n, r, m)) => { + let hn = Hnote {note: n.value().inner(), recipient: r.to_bytes().to_vec(), memo: m.as_slice().to_vec() }; + marshall_to_haskell_var(&hn, out, out_len, RW); + } + None => { + let hn0 = Hnote { note: 0, recipient: vec![0], memo: vec![0] }; + marshall_to_haskell_var(&hn0, out, out_len, RW); + } + } + }, + Err(_e1) => { let hn0 = Hnote { note: 0, recipient: vec![0], memo: vec![0] }; marshall_to_haskell_var(&hn0, out, out_len, RW); } -- 2.34.1 From 7992e5bfbe4e747d702f5bc6e27d85a7a9041ba4 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Mon, 2 Oct 2023 15:25:44 -0500 Subject: [PATCH 16/17] Expand fields of raw Tx parsing --- src/ZcashHaskell/Sapling.hs | 9 +++++++++ src/ZcashHaskell/Types.hs | 3 +++ 2 files changed, 12 insertions(+) diff --git a/src/ZcashHaskell/Sapling.hs b/src/ZcashHaskell/Sapling.hs index ac583cd..8207e75 100644 --- a/src/ZcashHaskell/Sapling.hs +++ b/src/ZcashHaskell/Sapling.hs @@ -57,6 +57,9 @@ instance FromJSON RawTxResponse where i <- obj .: "txid" o <- obj .:? "orchard" h <- obj .: "hex" + ht <- obj .: "height" + c <- obj .: "confirmations" + b <- obj .: "blocktime" case o of Nothing -> pure $ @@ -65,6 +68,9 @@ instance FromJSON RawTxResponse where (decodeHexText h) (getShieldedOutputs (decodeHexText h)) [] + ht + c + b Just o' -> do a <- o' .: "actions" pure $ @@ -73,3 +79,6 @@ instance FromJSON RawTxResponse where (decodeHexText h) (getShieldedOutputs (decodeHexText h)) a + ht + c + b diff --git a/src/ZcashHaskell/Types.hs b/src/ZcashHaskell/Types.hs index 51fe2c0..08dec78 100644 --- a/src/ZcashHaskell/Types.hs +++ b/src/ZcashHaskell/Types.hs @@ -105,6 +105,9 @@ data RawTxResponse = RawTxResponse , rt_hex :: BS.ByteString , rt_shieldedOutputs :: [BS.ByteString] , rt_orchardActions :: [OrchardAction] + , rt_blockheight :: Integer + , rt_confirmations :: Integer + , rt_blocktime :: Integer } deriving (Prelude.Show, Eq) -- * Sapling -- 2.34.1 From 1d558fc646a7758d60a721124812070de222c2e1 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Wed, 4 Oct 2023 11:12:30 -0500 Subject: [PATCH 17/17] Implement check of Unified Address and UVK --- CHANGELOG.md | 1 + librustzcash-wrapper/src/lib.rs | 38 ++++++++++++++++++++++++++++++++- src/C/Zcash.chs | 7 ++++++ src/ZcashHaskell/Orchard.hs | 5 +++++ test/Spec.hs | 15 +++++++++++++ 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3ad8c1..14e77d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `matchOrchardAddress` function to ensure a UA matches a UVK and corresponding tests - `makeZcashCall` function moved into this library - `RpcResponse`, `RpcCall` types moved into this library - Functions to decode Sapling transactions diff --git a/librustzcash-wrapper/src/lib.rs b/librustzcash-wrapper/src/lib.rs index f372a2e..27e30f9 100644 --- a/librustzcash-wrapper/src/lib.rs +++ b/librustzcash-wrapper/src/lib.rs @@ -47,7 +47,7 @@ use zcash_primitives::{ use zcash_address::{ Network, - unified::{Address, Encoding, Ufvk, Container, Fvk}, + unified::{Address, Encoding, Ufvk, Container, Fvk, Receiver}, ZcashAddress }; @@ -320,6 +320,42 @@ pub extern "C" fn rust_wrapper_svk_check_address( } } +#[no_mangle] +pub extern "C" fn rust_wrapper_ufvk_check_address( + key_input: *const u8, + key_input_len: usize, + address_input: *const u8, + address_input_len: usize + ) -> bool { + let key: String = marshall_from_haskell_var(key_input, key_input_len, RW); + let addy: String = marshall_from_haskell_var(address_input, address_input_len, RW); + let dec_key = Ufvk::decode(&key); + let dec_addy = Address::decode(&addy); + match dec_key { + Ok((n, ufvk)) => { + let i = ufvk.items(); + if let Fvk::Orchard(k) = i[0] { + let orch_key = FullViewingKey::from_bytes(&k).unwrap(); + let orch_addy = orch_key.address_at(0u32, Scope::External).to_raw_address_bytes(); + match dec_addy { + Ok((n, recs)) => { + let j = recs.items(); + j[0] == Receiver::Orchard(orch_addy) + }, + Err(_e) => { + false + } + } + } else { + false + } + }, + Err(_e) => { + false + } + } +} + #[no_mangle] pub extern "C" fn rust_wrapper_ufvk_decode( input: *const u8, diff --git a/src/C/Zcash.chs b/src/C/Zcash.chs index 67a5d31..a2e1ecd 100644 --- a/src/C/Zcash.chs +++ b/src/C/Zcash.chs @@ -72,6 +72,13 @@ import ZcashHaskell.Types -> `Bool' #} +{# fun pure unsafe rust_wrapper_ufvk_check_address as rustWrapperOrchardCheck + { toBorshVar* `BS.ByteString'& + , toBorshVar* `BS.ByteString'& + } + -> `Bool' +#} + {# fun unsafe rust_wrapper_sapling_note_decrypt_v2 as rustWrapperSaplingNoteDecode { toBorshVar* `BS.ByteString'& , toBorshVar* `BS.ByteString'& diff --git a/src/ZcashHaskell/Orchard.hs b/src/ZcashHaskell/Orchard.hs index edee2ff..d7c3665 100644 --- a/src/ZcashHaskell/Orchard.hs +++ b/src/ZcashHaskell/Orchard.hs @@ -13,6 +13,7 @@ module ZcashHaskell.Orchard where import C.Zcash ( rustWrapperIsUA + , rustWrapperOrchardCheck , rustWrapperOrchardNoteDecode , rustWrapperUfvkDecode ) @@ -33,6 +34,10 @@ decodeUfvk str = where decodedKey = (withPureBorshVarBuffer . rustWrapperUfvkDecode) str +-- | Check if the given UVK matches the UA given +matchOrchardAddress :: BS.ByteString -> BS.ByteString -> Bool +matchOrchardAddress = rustWrapperOrchardCheck + -- | Attempts to decode the given @OrchardAction@ using the given @UnifiedFullViewingKey@. decryptOrchardAction :: UnifiedFullViewingKey -> OrchardAction -> Maybe DecodedNote diff --git a/test/Spec.hs b/test/Spec.hs index 660d342..d61f92d 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -2,14 +2,17 @@ import C.Zcash (rustWrapperIsUA) import Data.Aeson +import Data.Bool (Bool(True)) import qualified Data.ByteString as BS import qualified Data.Text as T import qualified Data.Text.Encoding as E import qualified Data.Text.Lazy.Encoding as LE import qualified Data.Text.Lazy.IO as LTIO import Data.Word +import GHC.Float.RealFracMethods (properFractionDoubleInteger) import Test.Hspec import ZcashHaskell.Orchard +import ZcashHaskell.Orchard (matchOrchardAddress) import ZcashHaskell.Sapling ( decodeSaplingOutput , getShieldedOutputs @@ -28,6 +31,7 @@ import ZcashHaskell.Types , decodeHexText ) import ZcashHaskell.Utils +import ZcashHaskell.Utils (decodeBech32) main :: IO () main = do @@ -312,6 +316,17 @@ main = do let fakeUvk = "uview1u83changinga987bundchofch4ract3r5x8hqsw6vzw63n24atxpcatws82z092kryazuu6d7rayyut8m36wm4wpjy2z8r9hj48fx5pf49gw4sjrq8503qpz3vqj5hg0vg9vsqeasg5qjuyh94uyfm7v76udqcm2m0wfc25hcyqswcn56xxduq3xkgxkr0l73cjy88fdvf90eq5fda9g6x7yv7d0uckpevxg6540wc76xrc4axxvlt03ptaa2a0rektglmdy68656f3uzcdgqqyu0t7wk5cvwghyyvgqc0rp3vgu5ye4nd236ml57rjh083a2755qemf6dk6pw0qrnfm7246s8eg2hhzkzpf9h73chhng7xhmyem2sjh8rs2m9nhfcslsgenm" decodeUfvk fakeUvk `shouldBe` Nothing + describe "Check if UA and UVK match" $ do + let ua = + "u15hjz9v46azzmdept050heh8795qxzwy2pykg097lg69jpk4qzah90cj2q4amq0c07gta60x8qgw00qewcy3hg9kv9h6zjkh3jc66vr40u6uu2dxmqkqhypud95vm0gq7y5ga7c8psdqgthsrwvgd676a2pavpcd4euwwapgackxa3qhvga0wnl0k6vncskxlq94vqwjd7zepy3qd5jh" + let ua' = + "u17n7hpwaujyq7ux8f9jpyymtnk5urw7pyrf60smp5mawy7jgz325hfvz3jn3zsfya8yxryf9q7ldk8nu8df0emra5wne28zq9d9nm2pu4x6qwjha565av9aze0xgujgslz74ufkj0c0cylqwjyrh9msjfh7jzal6d3qzrnhkkqy3pqm8j63y07jxj7txqeac982778rmt64f32aum94x" + let uvk = + "uview1u833rp8yykd7h4druwht6xp6k8krle45fx8hqsw6vzw63n24atxpcatws82z092kryazuu6d7rayyut8m36wm4wpjy2z8r9hj48fx5pf49gw4sjrq8503qpz3vqj5hg0vg9vsqeasg5qjuyh94uyfm7v76udqcm2m0wfc25hcyqswcn56xxduq3xkgxkr0l73cjy88fdvf90eq5fda9g6x7yv7d0uckpevxg6540wc76xrc4axxvlt03ptaa2a0rektglmdy68656f3uzcdgqqyu0t7wk5cvwghyyvgqc0rp3vgu5ye4nd236ml57rjh083a2755qemf6dk6pw0qrnfm7246s8eg2hhzkzpf9h73chhng7xhmyem2sjh8rs2m9nhfcslsgenm" + it "succeeds with correct address" $ do + matchOrchardAddress uvk ua `shouldBe` True + it "fails with wrong address" $ do + matchOrchardAddress uvk ua' `shouldBe` False describe "Decode Sapling tx" $ do let svk = "zxviews1qvapd723qqqqpqq09ldgykvyusthmkky2w062esx5xg3nz4m29qxcvndyx6grrhrdepu4ns88sjr3u6mfp2hhwj5hfd6y24r0f64uwq65vjrmsh9mr568kenk33fcumag6djcjywkm5v295egjuk3qdd47atprs0j33nhaaqep3uqspzp5kg4mthugvug0sc3gc83atkrgmguw9g7gkvh82tugrntf66lnvyeh6ufh4j2xt0xr2r4zujtm3qvrmd3vvnulycuwqtetg2jk384" -- 2.34.1