diff --git a/backend-lib/Cargo.lock b/backend-lib/Cargo.lock index f07862ca3..2a5da0ca6 100644 --- a/backend-lib/Cargo.lock +++ b/backend-lib/Cargo.lock @@ -100,12 +100,80 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -693,6 +761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -701,8 +770,22 @@ version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ + "anstream 0.6.21", "anstyle", "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -731,6 +814,12 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "combine" version = "4.6.7" @@ -780,6 +869,26 @@ dependencies = [ "futures", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1391,6 +1500,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum-ordinalize" version = "3.1.15" @@ -1416,16 +1534,39 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_home" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream 1.0.0", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equihash" version = "0.2.2" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "blake2b_simd", "core2", @@ -1461,7 +1602,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.1" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "blake2b_simd", ] @@ -1566,6 +1707,30 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fpe" version = "0.6.1" @@ -1809,6 +1974,26 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "halo2_gadgets" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73a5e510d58a07d8ed238a5a8a436fe6c2c79e1bb2611f62688bc65007b4e6e7" +dependencies = [ + "arrayvec", + "bitvec", + "ff", + "group", + "halo2_poseidon", + "halo2_proofs", + "lazy_static", + "pasta_curves", + "rand 0.8.5", + "sinsemilla", + "subtle", + "uint", +] + [[package]] name = "halo2_gadgets" version = "0.4.0" @@ -2047,6 +2232,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -2060,12 +2260,29 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ + "base64", "bytes", "futures-channel", "futures-core", @@ -2073,12 +2290,16 @@ dependencies = [ "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2105,12 +2326,128 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "imt-tree" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d10acb863735c6c82c0c79e8752696096a86eb4d1e2fe0005ded4fae066297e" +dependencies = [ + "anyhow", + "ff", + "halo2_gadgets 0.3.1", + "hex", + "pasta_curves", + "rayon", +] + [[package]] name = "incrementalmerkletree" version = "0.8.2" @@ -2181,6 +2518,28 @@ dependencies = [ "rustversion", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -2205,6 +2564,30 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "jni" version = "0.21.1" @@ -2348,6 +2731,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "litrs" version = "1.0.0" @@ -2487,6 +2876,23 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk-sys" version = "0.5.0+25.2.9519653" @@ -2678,6 +3084,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oneshot-fused-workaround" version = "0.4.0" @@ -2699,6 +3111,60 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-src" +version = "300.6.0+3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2708,7 +3174,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orchard" version = "0.12.0" -source = "git+https://github.com/zcash/orchard.git?rev=6b12c77260aa7fac0d804983fc31b71b584d48e0#6b12c77260aa7fac0d804983fc31b71b584d48e0" +source = "git+https://github.com/valargroup/voting-circuits.git?branch=greg%2Forchard-0.12#b67b6b5b548a9dc8ebeb5b60572ca5201d2e7ed1" dependencies = [ "aes", "bitvec", @@ -2718,7 +3184,7 @@ dependencies = [ "fpe", "getset", "group", - "halo2_gadgets", + "halo2_gadgets 0.4.0", "halo2_poseidon", "halo2_proofs", "hex", @@ -2872,7 +3338,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pczt" version = "0.5.1" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "blake2b_simd", "bls12_381", @@ -2997,6 +3463,37 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pir-client" +version = "0.1.0" +source = "git+https://github.com/valargroup/vote-nullifier-pir.git?rev=d976e13#d976e13e5a69e5f03f0cede235af18c1257a6895" +dependencies = [ + "anyhow", + "ff", + "futures", + "hex", + "imt-tree", + "log", + "pasta_curves", + "pir-types", + "reqwest", + "serde_json", + "tokio", + "valar-ypir", +] + +[[package]] +name = "pir-types" +version = "0.1.0" +source = "git+https://github.com/valargroup/vote-nullifier-pir.git?rev=d976e13#d976e13e5a69e5f03f0cede235af18c1257a6895" +dependencies = [ + "anyhow", + "ff", + "imt-tree", + "pasta_curves", + "serde", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -3063,6 +3560,21 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postage" version = "0.5.0" @@ -3090,6 +3602,15 @@ dependencies = [ "serde", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3477,6 +3998,48 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "retry-error" version = "0.8.0" @@ -3714,6 +4277,15 @@ dependencies = [ "zip32", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.9.0" @@ -3806,7 +4378,30 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" dependencies = [ - "zeroize", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -3896,6 +4491,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.16.1" @@ -4153,6 +4760,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "static_assertions" version = "1.1.0" @@ -4225,6 +4838,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -4251,6 +4867,27 @@ dependencies = [ "windows", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -4270,6 +4907,38 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "test-log" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f46bf474f0a4afebf92f076d54fd5e63423d9438b8c278a3d2ccb0f47f7cdb3" +dependencies = [ + "env_logger", + "test-log-macros", + "tracing-subscriber", +] + +[[package]] +name = "test-log-core" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d4d41320b48bc4a211a9021678fcc0c99569b594ea31c93735b8e517102b4c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "test-log-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9beb9249a81e430dffd42400a49019bcf548444f1968ff23080a625de0d4d320" +dependencies = [ + "syn 2.0.111", + "test-log-core", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4413,6 +5082,16 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -5539,6 +6218,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -5727,6 +6424,30 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.19.0" @@ -5739,6 +6460,40 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valar-spiral-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c501f15052c202b2c9934226b1b16572e7ee6dfe77c014b6782e2580f205417a" +dependencies = [ + "fastrand", + "getrandom 0.2.16", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde_json", + "sha2 0.10.9", + "subtle", +] + +[[package]] +name = "valar-ypir" +version = "0.1.3" +source = "git+https://github.com/valargroup/ypir.git?branch=valar%2Fartifact#c2f70da921f35db8b98f41cc9ec40ff9e124ddc9" +dependencies = [ + "cc", + "clap", + "env_logger", + "fastrand", + "log", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", + "serde_json", + "sha1", + "test-log", + "valar-spiral-rs", +] + [[package]] name = "valuable" version = "0.1.1" @@ -5774,6 +6529,58 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "vote-commitment-tree" +version = "0.1.0" +source = "git+https://github.com/valargroup/zcash_voting.git?branch=greg%2Forchard-0.12#dfd848f9d8a967c1fef620027e47c6e8e7559cbf" +dependencies = [ + "anyhow", + "ff", + "halo2_gadgets 0.3.1", + "imt-tree", + "incrementalmerkletree", + "lazy_static", + "libc", + "pasta_curves", + "shardtree", +] + +[[package]] +name = "vote-commitment-tree-client" +version = "0.1.0" +source = "git+https://github.com/valargroup/zcash_voting.git?branch=greg%2Forchard-0.12#dfd848f9d8a967c1fef620027e47c6e8e7559cbf" +dependencies = [ + "base64", + "clap", + "ff", + "hex", + "pasta_curves", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.17", + "vote-commitment-tree", +] + +[[package]] +name = "voting-circuits" +version = "0.1.0" +source = "git+https://github.com/valargroup/voting-circuits.git?branch=greg%2Forchard-0.12#b67b6b5b548a9dc8ebeb5b60572ca5201d2e7ed1" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "halo2_gadgets 0.4.0", + "halo2_poseidon", + "halo2_proofs", + "incrementalmerkletree", + "lazy_static", + "orchard", + "pasta_curves", + "rand 0.8.5", + "sinsemilla", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -5830,6 +6637,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.106" @@ -6032,6 +6852,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -6320,6 +7151,12 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + [[package]] name = "wyz" version = "0.5.1" @@ -6356,6 +7193,29 @@ dependencies = [ "lzma-sys", ] +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] + [[package]] name = "zcash-android-wallet-sdk" version = "2.4.4" @@ -6365,12 +7225,15 @@ dependencies = [ "bytes", "dlopen2", "fs-mistrust", + "hex", "http", "http-body-util", + "incrementalmerkletree", "jni", "libc", "log-panics", "nonempty", + "openssl", "orchard", "paranoid-android", "pczt", @@ -6381,6 +7244,8 @@ dependencies = [ "rust_decimal", "sapling-crypto", "secrecy", + "serde", + "serde_json", "tonic", "tor-rtcompat", "tracing", @@ -6396,13 +7261,14 @@ dependencies = [ "zcash_protocol", "zcash_script", "zcash_transparent", + "zcash_voting", "zip32", ] [[package]] name = "zcash_address" version = "0.10.1" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "bech32", "bs58", @@ -6415,7 +7281,7 @@ dependencies = [ [[package]] name = "zcash_client_backend" version = "0.21.2" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "arti-client", "base64", @@ -6483,7 +7349,7 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.19.5" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "bip32", "bitflags 2.10.0", @@ -6529,7 +7395,7 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.3.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "core2", "hex", @@ -6539,7 +7405,7 @@ dependencies = [ [[package]] name = "zcash_keys" version = "0.12.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "bech32", "bip32", @@ -6581,7 +7447,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.26.4" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "blake2b_simd", "block-buffer 0.11.0-rc.3", @@ -6611,7 +7477,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.26.1" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "bellman", "blake2b_simd", @@ -6632,7 +7498,7 @@ dependencies = [ [[package]] name = "zcash_protocol" version = "0.7.2" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "core2", "document-features", @@ -6670,7 +7536,7 @@ dependencies = [ [[package]] name = "zcash_transparent" version = "0.6.3" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "bip32", "bs58", @@ -6691,6 +7557,38 @@ dependencies = [ "zip32", ] +[[package]] +name = "zcash_voting" +version = "0.1.0" +source = "git+https://github.com/valargroup/zcash_voting.git?branch=greg%2Forchard-0.12#dfd848f9d8a967c1fef620027e47c6e8e7559cbf" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "halo2_gadgets 0.3.1", + "halo2_proofs", + "hex", + "incrementalmerkletree", + "nonempty", + "orchard", + "pasta_curves", + "pczt", + "pir-client", + "rand 0.8.5", + "rusqlite", + "serde", + "serde_json", + "subtle", + "thiserror 2.0.17", + "vote-commitment-tree", + "vote-commitment-tree-client", + "voting-circuits", + "zcash_keys", + "zcash_primitives", + "zcash_protocol", + "zip32", +] + [[package]] name = "zerocopy" version = "0.8.31" @@ -6716,6 +7614,21 @@ name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] [[package]] name = "zeroize" @@ -6737,6 +7650,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" version = "0.11.5" @@ -6744,7 +7668,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "serde", + "yoke", "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -6763,7 +7700,7 @@ dependencies = [ [[package]] name = "zip321" version = "0.6.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=438657c52c7b5abd933c715e83f0289a79349c5a#438657c52c7b5abd933c715e83f0289a79349c5a" +source = "git+https://github.com/valargroup/librustzcash.git?branch=shielded-vote-main#1bc3a279e3c5504458a104c4295e192ad38a1e2a" dependencies = [ "base64", "nom", diff --git a/backend-lib/Cargo.toml b/backend-lib/Cargo.toml index fc21cefc4..39616ebd7 100644 --- a/backend-lib/Cargo.toml +++ b/backend-lib/Cargo.toml @@ -33,6 +33,23 @@ zcash_protocol = "0.7" zcash_script = "0.4" zip32 = "0.2" +# Shielded voting (valargroup/zcash_voting) +zcash_voting = { git = "https://github.com/valargroup/zcash_voting.git", branch = "greg/orchard-0.12" } + +# Serialization (needed for voting JSON FFI) +serde = { version = "1", features = ["derive"] } +serde_json = "1" +hex = "0.4" + +# Merkle tree +incrementalmerkletree = "0.8" + +# OpenSSL vendored — needed for cross-compilation to Android targets. +# pir-client (via zcash_voting) pulls in reqwest with native-tls default, +# which requires OpenSSL. Using the vendored feature compiles OpenSSL from +# source with the NDK toolchain instead of looking for a system library. +openssl = { version = "0.10", features = ["vendored"] } + # Infrastructure prost = "0.14" rusqlite = "0.37" @@ -102,14 +119,18 @@ path = "src/main/rust/lib.rs" crate-type = ["staticlib", "cdylib"] [patch.crates-io] -pczt = { git = "https://github.com/zcash/librustzcash.git", rev = "438657c52c7b5abd933c715e83f0289a79349c5a" } -orchard = { package = "orchard", git = "https://github.com/zcash/orchard.git", rev = "6b12c77260aa7fac0d804983fc31b71b584d48e0" } +# Voting-circuits fork of orchard — upstream 0.12 + pub visibility for constants/spec +# + shared_primitives gadget for voting ZKPs (same as iOS SDK shielded-vote-2.4.10). +orchard = { package = "orchard", git = "https://github.com/valargroup/voting-circuits.git", branch = "greg/orchard-0.12" } sapling = { package = "sapling-crypto", git = "https://github.com/zcash/sapling-crypto.git", rev = "4f95c2286dbe90f05e6f44d634bc2924b992fab4" } -transparent = { package = "zcash_transparent", git = "https://github.com/zcash/librustzcash.git", rev = "438657c52c7b5abd933c715e83f0289a79349c5a" } -zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "438657c52c7b5abd933c715e83f0289a79349c5a" } -zcash_client_backend = { git = "https://github.com/zcash/librustzcash.git", rev = "438657c52c7b5abd933c715e83f0289a79349c5a" } -zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = "438657c52c7b5abd933c715e83f0289a79349c5a" } -zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "438657c52c7b5abd933c715e83f0289a79349c5a" } -zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "438657c52c7b5abd933c715e83f0289a79349c5a" } -zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "438657c52c7b5abd933c715e83f0289a79349c5a" } -zcash_protocol = { git = "https://github.com/zcash/librustzcash.git", rev = "438657c52c7b5abd933c715e83f0289a79349c5a" } +# valargroup librustzcash fork — shielded-vote-main branch adds PCZT/key visibility +# changes required for voting delegation flows (mirrors iOS SDK patches). +pczt = { git = "https://github.com/valargroup/librustzcash.git", branch = "shielded-vote-main" } +transparent = { package = "zcash_transparent", git = "https://github.com/valargroup/librustzcash.git", branch = "shielded-vote-main" } +zcash_address = { git = "https://github.com/valargroup/librustzcash.git", branch = "shielded-vote-main" } +zcash_client_backend = { git = "https://github.com/valargroup/librustzcash.git", branch = "shielded-vote-main" } +zcash_client_sqlite = { git = "https://github.com/valargroup/librustzcash.git", branch = "shielded-vote-main" } +zcash_keys = { git = "https://github.com/valargroup/librustzcash.git", branch = "shielded-vote-main" } +zcash_primitives = { git = "https://github.com/valargroup/librustzcash.git", branch = "shielded-vote-main" } +zcash_proofs = { git = "https://github.com/valargroup/librustzcash.git", branch = "shielded-vote-main" } +zcash_protocol = { git = "https://github.com/valargroup/librustzcash.git", branch = "shielded-vote-main" } diff --git a/backend-lib/build.gradle.kts b/backend-lib/build.gradle.kts index 8b32166dd..06242ebc4 100644 --- a/backend-lib/build.gradle.kts +++ b/backend-lib/build.gradle.kts @@ -85,6 +85,13 @@ cargo { // https://developer.android.com/about/versions/15/behavior-changes-all#16-kb exec = { spec, _ -> spec.environment["RUST_ANDROID_GRADLE_CC_LINK_ARG"] = "-Wl,-z,max-page-size=16384" + // NDK >= r23 ships llvm-ranlib instead of target-prefixed ranlib wrappers. + // openssl-sys vendored build looks for "arm-linux-androideabi-ranlib" which + // doesn't exist; point it at llvm-ranlib so OpenSSL cross-compiles correctly. + val ndkDir = android.ndkDirectory + val ndkBin = "$ndkDir/toolchains/llvm/prebuilt/darwin-x86_64/bin" + spec.environment["RANLIB"] = "$ndkBin/llvm-ranlib" + spec.environment["AR"] = "$ndkBin/llvm-ar" } } diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/JniConstants.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/JniConstants.kt index 1158c5827..b17c381a3 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/JniConstants.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/JniConstants.kt @@ -22,3 +22,13 @@ const val JNI_METADATA_KEY_SK_SIZE = 32 * The number of bytes in a chain code. It's used e.g. in [JniMetadataKey.chainCode] */ const val JNI_METADATA_KEY_CHAIN_CODE_SIZE = 32 + +/** + * The number of bytes in a voting hotkey secret key. It's used e.g. in [HotkeySecretKey.value] + */ +const val JNI_HOTKEY_SECRET_KEY_BYTES_SIZE = 32 + +/** + * The number of bytes in a voting hotkey public key. It's used e.g. in [HotkeyPublicKey.value] + */ +const val JNI_HOTKEY_PUBLIC_KEY_BYTES_SIZE = 32 diff --git a/backend-lib/src/main/rust/lib.rs b/backend-lib/src/main/rust/lib.rs index 632598ae7..fb74f6e52 100644 --- a/backend-lib/src/main/rust/lib.rs +++ b/backend-lib/src/main/rust/lib.rs @@ -102,6 +102,7 @@ use crate::utils::{ mod tor; mod utils; +mod voting; #[cfg(debug_assertions)] fn print_debug_state() { diff --git a/backend-lib/src/main/rust/voting.rs b/backend-lib/src/main/rust/voting.rs new file mode 100644 index 000000000..3d0a16a29 --- /dev/null +++ b/backend-lib/src/main/rust/voting.rs @@ -0,0 +1,1570 @@ +//! JNI bindings for the zcash_voting crate. +//! +//! Mirrors rust/src/voting.rs from valargroup/zcash-swift-wallet-sdk (shielded-vote-2.4.10), +//! translating C FFI (`zcashlc_voting_*`) to Android JNI +//! (`Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_*`). +//! +//! Key differences from iOS: +//! - JNI types (JString, JByteArray) instead of raw C pointer + length pairs. +//! - Opaque handles encoded as `jlong` (Box::into_raw as i64) instead of *mut ptr. +//! - `catch_unwind` + `unwrap_exc_or` instead of `catch_panic` + `unwrap_exc_or_null`. +//! - JSON serialisation uses local JsonXxx wrapper types because zcash_voting types +//! do not derive Serialize/Deserialize (by design — EncryptedShare has secret fields). +//! - Progress reporting uses `NoopProgressReporter` (no JNI callback needed for MVP). + +use std::sync::Arc; + +use anyhow::anyhow; +use jni::{ + JNIEnv, + objects::{JByteArray, JClass, JObject, JString, JValue}, + sys::{JNI_FALSE, JNI_TRUE, jboolean, jbyteArray, jint, jlong, jobject, jstring}, +}; +use orchard::keys::Scope; +use serde::{Deserialize, Serialize}; +use zcash_client_backend::keys::{UnifiedFullViewingKey, UnifiedSpendingKey}; +use zcash_protocol::consensus::Network; + +use zcash_voting as voting; +use voting::storage::{RoundPhase, RoundState, RoundSummary, VotingDb}; +use voting::tree_sync::VoteTreeSync; +use voting::types::{ + DelegationProofResult, DelegationSubmissionData, GovernancePczt, NoteInfo, NoopProgressReporter, + SharePayload, VoteCommitmentBundle, WireEncryptedShare, WitnessData, +}; + +use crate::utils::{ + catch_unwind, exception::unwrap_exc_or, java_nullable_string_to_rust, java_string_to_rust, +}; + +// ============================================================================= +// Opaque handle +// ============================================================================= + +pub(crate) struct VotingDatabaseHandle { + db: Arc, + tree_sync: VoteTreeSync, +} + +fn handle_from_jlong(handle: jlong) -> anyhow::Result<&'static VotingDatabaseHandle> { + if handle == 0 { + return Err(anyhow!("VotingDatabaseHandle is null")); + } + // SAFETY: pointer was allocated by openVotingDb via Box::into_raw. + Ok(unsafe { &*(handle as *const VotingDatabaseHandle) }) +} + +fn network_from_id(id: jint) -> anyhow::Result { + match id { + 0 => Ok(Network::MainNetwork), + 1 => Ok(Network::TestNetwork), + _ => Err(anyhow!("invalid network_id {}", id)), + } +} + +/// Compute the 43-byte raw Orchard address for a hotkey seed. +/// +/// The governance PCZT needs the hotkey's Orchard address as the delegation output. +/// This is distinct from VotingHotkey.address (which is a Pallas-based address used +/// for vote commitment signing). +fn hotkey_orchard_raw_address( + hotkey_seed: &[u8], + network: Network, + account_index: u32, +) -> anyhow::Result> { + let account_id = zip32::AccountId::try_from(account_index) + .map_err(|_| anyhow!("invalid account_index {}", account_index))?; + let usk = UnifiedSpendingKey::from_seed(&network, hotkey_seed, account_id) + .map_err(|e| anyhow!("failed to derive hotkey USK: {}", e))?; + let fvk = usk.to_unified_full_viewing_key(); + let orchard_fvk = fvk + .orchard() + .ok_or_else(|| anyhow!("hotkey UFVK has no Orchard component"))?; + let addr = orchard_fvk.address_at(0u32, Scope::External); + Ok(addr.to_raw_address_bytes().to_vec()) +} + +/// Extract 96-byte Orchard FVK bytes from a UFVK string. +fn orchard_fvk_bytes(ufvk_str: &str, network: Network) -> anyhow::Result> { + let ufvk = UnifiedFullViewingKey::decode(&network, ufvk_str) + .map_err(|e| anyhow!("failed to decode UFVK: {}", e))?; + let fvk = ufvk + .orchard() + .ok_or_else(|| anyhow!("UFVK has no Orchard component"))?; + Ok(fvk.to_bytes().to_vec()) +} + +/// NU6 consensus branch ID (same on mainnet and testnet). +const NU6_BRANCH_ID: u32 = 0xC8E71055; + +fn coin_type_for(network: Network) -> u32 { + match network { + Network::MainNetwork => 133, + Network::TestNetwork => 1, + } +} + +// ============================================================================= +// JSON wrapper types +// +// The zcash_voting crate types do not derive Serialize/Deserialize. +// We use local wrapper types for crossing the JNI boundary as JSON strings. +// Byte arrays are encoded as lowercase hex strings to match the iOS FFI layer. +// ============================================================================= + +/// Hex-encode helper. +fn hex_enc(b: &[u8]) -> String { + hex::encode(b) +} + +/// Hex-decode helper. +fn hex_dec(s: &str, field: &str) -> anyhow::Result> { + hex::decode(s).map_err(|e| anyhow!("field '{}': invalid hex: {}", field, e)) +} + +#[derive(Serialize, Deserialize)] +struct JsonNoteInfo { + commitment: String, // hex + nullifier: String, // hex + value: u64, + position: u64, + diversifier: String, // hex + rho: String, // hex + rseed: String, // hex + scope: u32, + ufvk_str: String, +} + +impl TryFrom for NoteInfo { + type Error = anyhow::Error; + fn try_from(j: JsonNoteInfo) -> anyhow::Result { + Ok(NoteInfo { + commitment: hex_dec(&j.commitment, "commitment")?, + nullifier: hex_dec(&j.nullifier, "nullifier")?, + value: j.value, + position: j.position, + diversifier: hex_dec(&j.diversifier, "diversifier")?, + rho: hex_dec(&j.rho, "rho")?, + rseed: hex_dec(&j.rseed, "rseed")?, + scope: j.scope, + ufvk_str: j.ufvk_str, + }) + } +} + +#[derive(Serialize, Deserialize)] +struct JsonWitnessData { + note_commitment: String, // hex + position: u64, + root: String, // hex + auth_path: Vec, // hex elements +} + +impl TryFrom for WitnessData { + type Error = anyhow::Error; + fn try_from(j: JsonWitnessData) -> anyhow::Result { + Ok(WitnessData { + note_commitment: hex_dec(&j.note_commitment, "note_commitment")?, + position: j.position, + root: hex_dec(&j.root, "root")?, + auth_path: j + .auth_path + .iter() + .enumerate() + .map(|(i, h)| hex_dec(h, &format!("auth_path[{i}]"))) + .collect::>()?, + }) + } +} + +impl From for JsonWitnessData { + fn from(w: WitnessData) -> Self { + JsonWitnessData { + note_commitment: hex_enc(&w.note_commitment), + position: w.position, + root: hex_enc(&w.root), + auth_path: w.auth_path.iter().map(|h| hex_enc(h)).collect(), + } + } +} + +/// VanWitness JSON (returned by generateVanWitnessJson / passed to buildVoteCommitmentJson). +#[derive(Serialize, Deserialize)] +struct JsonVanWitness { + auth_path: Vec, // hex, each 32 bytes + position: u32, + anchor_height: u32, +} + +impl From for JsonVanWitness { + fn from(w: voting::tree_sync::VanWitness) -> Self { + JsonVanWitness { + auth_path: w.auth_path.iter().map(|h| hex_enc(h)).collect(), + position: w.position, + anchor_height: w.anchor_height, + } + } +} + +#[derive(Serialize)] +struct JsonRoundSummary { + round_id: String, + phase: u32, + snapshot_height: u64, + created_at: u64, +} + +impl From for JsonRoundSummary { + fn from(r: RoundSummary) -> Self { + JsonRoundSummary { + round_id: r.round_id, + phase: round_phase_to_u32(r.phase), + snapshot_height: r.snapshot_height, + created_at: r.created_at, + } + } +} + +fn round_phase_to_u32(p: RoundPhase) -> u32 { + match p { + RoundPhase::Initialized => 0, + RoundPhase::HotkeyGenerated => 1, + RoundPhase::DelegationConstructed => 2, + RoundPhase::DelegationProved => 3, + RoundPhase::VoteReady => 4, + } +} + +#[derive(Serialize)] +struct JsonGovernancePczt { + pczt_bytes: String, + rk: String, + alpha: String, + nf_signed: String, + cmx_new: String, + gov_nullifiers: Vec, + van: String, + van_comm_rand: String, + dummy_nullifiers: Vec, + rho_signed: String, + padded_cmx: Vec, + rseed_signed: String, + rseed_output: String, + action_bytes: String, + action_index: u32, + padded_note_secrets: Vec<[String; 2]>, // [[rho_hex, rseed_hex], ...] + pczt_sighash: String, +} + +impl From for JsonGovernancePczt { + fn from(g: GovernancePczt) -> Self { + JsonGovernancePczt { + pczt_bytes: hex_enc(&g.pczt_bytes), + rk: hex_enc(&g.rk), + alpha: hex_enc(&g.alpha), + nf_signed: hex_enc(&g.nf_signed), + cmx_new: hex_enc(&g.cmx_new), + gov_nullifiers: g.gov_nullifiers.iter().map(|v| hex_enc(v)).collect(), + van: hex_enc(&g.van), + van_comm_rand: hex_enc(&g.van_comm_rand), + dummy_nullifiers: g.dummy_nullifiers.iter().map(|v| hex_enc(v)).collect(), + rho_signed: hex_enc(&g.rho_signed), + padded_cmx: g.padded_cmx.iter().map(|v| hex_enc(v)).collect(), + rseed_signed: hex_enc(&g.rseed_signed), + rseed_output: hex_enc(&g.rseed_output), + action_bytes: hex_enc(&g.action_bytes), + action_index: g.action_index as u32, + padded_note_secrets: g + .padded_note_secrets + .iter() + .map(|(rho, rseed)| [hex_enc(rho), hex_enc(rseed)]) + .collect(), + pczt_sighash: hex_enc(&g.pczt_sighash), + } + } +} + +#[derive(Serialize)] +struct JsonDelegationProofResult { + proof: String, + public_inputs: Vec, + nf_signed: String, + cmx_new: String, + gov_nullifiers: Vec, + van_comm: String, + rk: String, +} + +impl From for JsonDelegationProofResult { + fn from(r: DelegationProofResult) -> Self { + JsonDelegationProofResult { + proof: hex_enc(&r.proof), + public_inputs: r.public_inputs.iter().map(|v| hex_enc(v)).collect(), + nf_signed: hex_enc(&r.nf_signed), + cmx_new: hex_enc(&r.cmx_new), + gov_nullifiers: r.gov_nullifiers.iter().map(|v| hex_enc(v)).collect(), + van_comm: hex_enc(&r.van_comm), + rk: hex_enc(&r.rk), + } + } +} + +#[derive(Serialize, Deserialize)] +struct JsonWireEncryptedShare { + c1: String, // hex + c2: String, // hex + share_index: u32, +} + +impl TryFrom for WireEncryptedShare { + type Error = anyhow::Error; + fn try_from(j: JsonWireEncryptedShare) -> anyhow::Result { + Ok(WireEncryptedShare { + c1: hex_dec(&j.c1, "c1")?, + c2: hex_dec(&j.c2, "c2")?, + share_index: j.share_index, + }) + } +} + +/// VoteCommitmentBundle — serialized for Kotlin; only wire-safe share fields included. +#[derive(Serialize, Deserialize)] +struct JsonVoteCommitmentBundle { + van_nullifier: String, + vote_authority_note_new: String, + vote_commitment: String, + proposal_id: u32, + proof: String, + /// Wire-safe shares only (c1, c2, share_index — no secret plaintext or randomness). + enc_shares: Vec, + anchor_height: u32, + vote_round_id: String, + shares_hash: String, + share_blinds: Vec, + share_comms: Vec, + r_vpk_bytes: String, + alpha_v: String, +} + +impl From<&VoteCommitmentBundle> for JsonVoteCommitmentBundle { + fn from(b: &VoteCommitmentBundle) -> Self { + JsonVoteCommitmentBundle { + van_nullifier: hex_enc(&b.van_nullifier), + vote_authority_note_new: hex_enc(&b.vote_authority_note_new), + vote_commitment: hex_enc(&b.vote_commitment), + proposal_id: b.proposal_id, + proof: hex_enc(&b.proof), + enc_shares: b + .enc_shares + .iter() + .map(|s| JsonWireEncryptedShare { + c1: hex_enc(&s.c1), + c2: hex_enc(&s.c2), + share_index: s.share_index, + }) + .collect(), + anchor_height: b.anchor_height, + vote_round_id: b.vote_round_id.clone(), + shares_hash: hex_enc(&b.shares_hash), + share_blinds: b.share_blinds.iter().map(|v| hex_enc(v)).collect(), + share_comms: b.share_comms.iter().map(|v| hex_enc(v)).collect(), + r_vpk_bytes: hex_enc(&b.r_vpk_bytes), + alpha_v: hex_enc(&b.alpha_v), + } + } +} + +#[derive(Serialize)] +struct JsonSharePayload { + shares_hash: String, + proposal_id: u32, + vote_decision: u32, + enc_share: JsonWireEncryptedShare, + tree_position: u64, + all_enc_shares: Vec, + share_comms: Vec, + primary_blind: String, +} + +impl From for JsonSharePayload { + fn from(p: SharePayload) -> Self { + JsonSharePayload { + shares_hash: hex_enc(&p.shares_hash), + proposal_id: p.proposal_id, + vote_decision: p.vote_decision, + enc_share: JsonWireEncryptedShare { + c1: hex_enc(&p.enc_share.c1), + c2: hex_enc(&p.enc_share.c2), + share_index: p.enc_share.share_index, + }, + tree_position: p.tree_position, + all_enc_shares: p + .all_enc_shares + .iter() + .map(|s| JsonWireEncryptedShare { + c1: hex_enc(&s.c1), + c2: hex_enc(&s.c2), + share_index: s.share_index, + }) + .collect(), + share_comms: p.share_comms.iter().map(|v| hex_enc(v)).collect(), + primary_blind: hex_enc(&p.primary_blind), + } + } +} + +#[derive(Serialize)] +struct JsonDelegationSubmission { + proof: String, + rk: String, + spend_auth_sig: String, + sighash: String, + nf_signed: String, + cmx_new: String, + gov_comm: String, + gov_nullifiers: Vec, + alpha: String, + vote_round_id: String, +} + +impl From for JsonDelegationSubmission { + fn from(d: DelegationSubmissionData) -> Self { + JsonDelegationSubmission { + proof: hex_enc(&d.proof), + rk: hex_enc(&d.rk), + spend_auth_sig: hex_enc(&d.spend_auth_sig), + sighash: hex_enc(&d.sighash), + nf_signed: hex_enc(&d.nf_signed), + cmx_new: hex_enc(&d.cmx_new), + gov_comm: hex_enc(&d.gov_comm), + gov_nullifiers: d.gov_nullifiers.iter().map(|v| hex_enc(v)).collect(), + alpha: hex_enc(&d.alpha), + vote_round_id: d.vote_round_id, + } + } +} + +// Convenience: deserialise a JSON string into T, throwing a JNI exception on error. +// Lifetimes are left implicit so this works inside catch_unwind closures. +fn json_from_jstring Deserialize<'de>>( + env: &mut JNIEnv<'_>, + js: &JString<'_>, + field: &str, +) -> anyhow::Result { + let s = java_string_to_rust(env, js)?; + serde_json::from_str(&s).map_err(|e| anyhow!("{} JSON parse error: {}", field, e)) +} + +fn json_to_jstring(env: &mut JNIEnv<'_>, value: &T) -> anyhow::Result { + let s = serde_json::to_string(value) + .map_err(|e| anyhow!("JSON serialization error: {}", e))?; + Ok(env.new_string(s)?.into_raw()) +} + +// ============================================================================= +// A. Database lifecycle +// ============================================================================= + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_openVotingDb< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_path: JString<'local>, +) -> jlong { + let res = catch_unwind(&mut env, |env| { + let path = java_string_to_rust(env, &db_path)?; + let db = VotingDb::open(&path) + .map_err(|e| anyhow!("VotingDb::open failed: {}", e))?; + let handle = Box::new(VotingDatabaseHandle { + db: Arc::new(db), + tree_sync: VoteTreeSync::new(), + }); + Ok(Box::into_raw(handle) as jlong) + }); + unwrap_exc_or(&mut env, res, 0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_closeVotingDb< + 'local, +>( + mut _env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, +) { + if db_handle != 0 { + unsafe { drop(Box::from_raw(db_handle as *mut VotingDatabaseHandle)) }; + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_setWalletId< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + wallet_id: JString<'local>, +) -> jboolean { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let id = java_string_to_rust(env, &wallet_id)?; + handle.db.set_wallet_id(&id); + Ok(JNI_TRUE) + }); + unwrap_exc_or(&mut env, res, JNI_FALSE) +} + +// ============================================================================= +// B. Round management +// ============================================================================= + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_initRound< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + snapshot_height: jlong, + ea_pk: JByteArray<'local>, + nc_root: JByteArray<'local>, + nullifier_imt_root: JByteArray<'local>, + session_json: JString<'local>, +) -> jboolean { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let params = voting::types::VotingRoundParams { + vote_round_id: java_string_to_rust(env, &round_id)?, + snapshot_height: snapshot_height as u64, + ea_pk: env.convert_byte_array(&ea_pk)?, + nc_root: env.convert_byte_array(&nc_root)?, + nullifier_imt_root: env.convert_byte_array(&nullifier_imt_root)?, + }; + let session = java_nullable_string_to_rust(env, &session_json)?; + handle + .db + .init_round(¶ms, session.as_deref()) + .map_err(|e| anyhow!("init_round: {}", e))?; + Ok(JNI_TRUE) + }); + unwrap_exc_or(&mut env, res, JNI_FALSE) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_getRoundState< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, +) -> jobject { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let state = handle + .db + .get_round_state(&java_string_to_rust(env, &round_id)?) + .map_err(|e| anyhow!("get_round_state: {}", e))?; + make_ffi_round_state(env, state) + }); + unwrap_exc_or(&mut env, res, JObject::null().into_raw()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_listRoundsJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let rounds: Vec = handle + .db + .list_rounds() + .map_err(|e| anyhow!("list_rounds: {}", e))? + .into_iter() + .map(JsonRoundSummary::from) + .collect(); + json_to_jstring(env, &rounds) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_clearRound< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, +) -> jboolean { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + handle + .db + .clear_round(&java_string_to_rust(env, &round_id)?) + .map_err(|e| anyhow!("clear_round: {}", e))?; + Ok(JNI_TRUE) + }); + unwrap_exc_or(&mut env, res, JNI_FALSE) +} + +// ============================================================================= +// C. Note setup +// ============================================================================= + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_setupBundles< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + notes_json: JString<'local>, +) -> jobject { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let json_notes: Vec = + json_from_jstring(env, ¬es_json, "notesJson")?; + let notes: Vec = json_notes + .into_iter() + .map(NoteInfo::try_from) + .collect::>()?; + let (count, weight) = handle + .db + .setup_bundles(&java_string_to_rust(env, &round_id)?, ¬es) + .map_err(|e| anyhow!("setup_bundles: {}", e))?; + make_ffi_bundle_setup_result(env, count, weight) + }); + unwrap_exc_or(&mut env, res, JObject::null().into_raw()) +} + +// ============================================================================= +// D. Hotkey generation +// ============================================================================= + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_generateHotkey< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + seed: JByteArray<'local>, +) -> jobject { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let hotkey = handle + .db + .generate_hotkey( + &java_string_to_rust(env, &round_id)?, + &env.convert_byte_array(&seed)?, + ) + .map_err(|e| anyhow!("generate_hotkey: {}", e))?; + make_ffi_voting_hotkey(env, hotkey) + }); + unwrap_exc_or(&mut env, res, JObject::null().into_raw()) +} + +// ============================================================================= +// E. Governance PCZT +// ============================================================================= + +/// Builds the governance PCZT for bundle [bundleIndex]. +/// +/// Additional parameters vs the simplified original Kotlin API: +/// - [notesJson] JSON array of NoteInfo for this bundle +/// - [hotkeyRawSeed] 32-byte hotkey seed — used to derive the 43-byte Orchard hotkey address +/// - [seedFingerprint] 32-byte ZIP-32 seed fingerprint of the wallet +/// - [roundName] Human-readable round name (from session data) +/// - [addressIndex] ZIP-32 address index (normally 0) +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_buildGovernancePcztJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + bundle_index: jint, + ufvk: JString<'local>, + network_id: jint, + account_index: jint, + notes_json: JString<'local>, + hotkey_raw_seed: JByteArray<'local>, + seed_fingerprint: JByteArray<'local>, + round_name: JString<'local>, + address_index: jint, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let network = network_from_id(network_id)?; + let ufvk_str = java_string_to_rust(env, &ufvk)?; + let fvk_bytes = orchard_fvk_bytes(&ufvk_str, network)?; + + let seed_bytes = env.convert_byte_array(&hotkey_raw_seed)?; + let hotkey_raw_address = + hotkey_orchard_raw_address(&seed_bytes, network, account_index as u32)?; + + let sf_bytes = env.convert_byte_array(&seed_fingerprint)?; + let sf_arr: [u8; 32] = sf_bytes + .as_slice() + .try_into() + .map_err(|_| anyhow!("seedFingerprint must be 32 bytes"))?; + + let json_notes: Vec = + json_from_jstring(env, ¬es_json, "notesJson")?; + let notes: Vec = json_notes + .into_iter() + .map(NoteInfo::try_from) + .collect::>()?; + + let round_id_str = java_string_to_rust(env, &round_id)?; + let round_name_str = java_string_to_rust(env, &round_name)?; + + let pczt = handle + .db + .build_governance_pczt( + &round_id_str, + bundle_index as u32, + ¬es, + &fvk_bytes, + &hotkey_raw_address, + NU6_BRANCH_ID, + coin_type_for(network), + &sf_arr, + account_index as u32, + &round_name_str, + address_index as u32, + ) + .map_err(|e| anyhow!("build_governance_pczt: {}", e))?; + + json_to_jstring(env, &JsonGovernancePczt::from(pczt)) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_extractPcztSighash< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + pczt_bytes: JByteArray<'local>, +) -> jbyteArray { + let res = catch_unwind(&mut env, |env| { + let bytes = env.convert_byte_array(&pczt_bytes)?; + let sighash = voting::action::extract_pczt_sighash(&bytes) + .map_err(|e| anyhow!("extract_pczt_sighash: {}", e))?; + Ok(env.byte_array_from_slice(&sighash)?.into_raw()) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +// ============================================================================= +// F. Witness management +// ============================================================================= + +/// Cache Merkle witnesses for notes in a bundle (must be called before buildAndProveDelegationJson). +/// +/// [witnessesJson] JSON array of WitnessData objects (one per note in the bundle). +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_storeWitnesses< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + bundle_index: jint, + witnesses_json: JString<'local>, +) -> jboolean { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let json_witnesses: Vec = + json_from_jstring(env, &witnesses_json, "witnessesJson")?; + let witnesses: Vec = json_witnesses + .into_iter() + .map(WitnessData::try_from) + .collect::>()?; + handle + .db + .store_witnesses( + &java_string_to_rust(env, &round_id)?, + bundle_index as u32, + &witnesses, + ) + .map_err(|e| anyhow!("store_witnesses: {}", e))?; + Ok(JNI_TRUE) + }); + unwrap_exc_or(&mut env, res, JNI_FALSE) +} + +// ============================================================================= +// G. Delegation proof (ZKP1) +// ============================================================================= + +/// Generates the Halo2 delegation proof. Long-running — call on a background coroutine. +/// +/// [notesJson] JSON array of NoteInfo for this bundle (same notes as storeWitnesses) +/// [hotkeyRawSeed] 32-byte hotkey seed — used to re-derive hotkey Orchard address +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_buildAndProveDelegationJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + bundle_index: jint, + pir_server_url: JString<'local>, + network_id: jint, + notes_json: JString<'local>, + hotkey_raw_seed: JByteArray<'local>, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let network = network_from_id(network_id)?; + + let seed_bytes = env.convert_byte_array(&hotkey_raw_seed)?; + let hotkey_raw_address = hotkey_orchard_raw_address(&seed_bytes, network, 0)?; + + let json_notes: Vec = + json_from_jstring(env, ¬es_json, "notesJson")?; + let notes: Vec = json_notes + .into_iter() + .map(NoteInfo::try_from) + .collect::>()?; + + let result = handle + .db + .build_and_prove_delegation( + &java_string_to_rust(env, &round_id)?, + bundle_index as u32, + ¬es, + &hotkey_raw_address, + &java_string_to_rust(env, &pir_server_url)?, + network_id as u32, + &NoopProgressReporter, + ) + .map_err(|e| anyhow!("build_and_prove_delegation: {}", e))?; + + json_to_jstring(env, &JsonDelegationProofResult::from(result)) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +// ============================================================================= +// H. Vote commitment tree (VAN witness) +// ============================================================================= + +/// Synchronise the per-round vote commitment tree from the chain node. +/// +/// Returns the latest synced block height as a Long, or -1 on error. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_syncVoteTree< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + node_url: JString<'local>, +) -> jlong { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let height = handle + .tree_sync + .sync( + &handle.db, + &java_string_to_rust(env, &round_id)?, + &java_string_to_rust(env, &node_url)?, + ) + .map_err(|e| anyhow!("sync_vote_tree: {}", e))?; + Ok(height as jlong) + }); + unwrap_exc_or(&mut env, res, -1) +} + +/// Store the VAN leaf position after the delegation TX is confirmed. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_storeVanPosition< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + bundle_index: jint, + position: jint, +) -> jboolean { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + handle + .db + .store_van_position( + &java_string_to_rust(env, &round_id)?, + bundle_index as u32, + position as u32, + ) + .map_err(|e| anyhow!("store_van_position: {}", e))?; + Ok(JNI_TRUE) + }); + unwrap_exc_or(&mut env, res, JNI_FALSE) +} + +/// Generate the Merkle witness for the VAN note. Must be called after [syncVoteTree]. +/// +/// Returns JSON-encoded VanWitness, or null on error. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_generateVanWitnessJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + bundle_index: jint, + anchor_height: jint, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let witness = handle + .tree_sync + .generate_van_witness( + &handle.db, + &java_string_to_rust(env, &round_id)?, + bundle_index as u32, + anchor_height as u32, + ) + .map_err(|e| anyhow!("generate_van_witness: {}", e))?; + json_to_jstring(env, &JsonVanWitness::from(witness)) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +// ============================================================================= +// I. Vote commitment (ZKP2) +// ============================================================================= + +/// Build the vote commitment proof for one proposal choice. +/// +/// [witnessJson] JSON-encoded VanWitness returned by [generateVanWitnessJson] +/// [singleShare] true for single-share mode (test/dev only) +/// +/// Returns JSON-encoded VoteCommitmentBundle (wire-safe — no secret share fields). +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_buildVoteCommitmentJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + bundle_index: jint, + hotkey_seed: JByteArray<'local>, + proposal_id: jint, + choice: jint, + num_options: jint, + witness_json: JString<'local>, + van_position: jint, + anchor_height: jint, + network_id: jint, + single_share: jboolean, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let seed = env.convert_byte_array(&hotkey_seed)?; + + // Extract auth_path from VanWitness JSON (24 × 32-byte sibling hashes). + let van: JsonVanWitness = json_from_jstring(env, &witness_json, "witnessJson")?; + let auth_path: Vec<[u8; 32]> = van + .auth_path + .iter() + .enumerate() + .map(|(i, h)| { + let bytes = hex_dec(h, &format!("auth_path[{i}]"))?; + bytes + .try_into() + .map_err(|_| anyhow!("auth_path[{i}] must be 32 bytes")) + }) + .collect::>()?; + + let bundle = handle + .db + .build_vote_commitment( + &java_string_to_rust(env, &round_id)?, + bundle_index as u32, + &seed, + network_id as u32, + proposal_id as u32, + choice as u32, + num_options as u32, + &auth_path, + van_position as u32, + anchor_height as u32, + single_share == JNI_TRUE, + &NoopProgressReporter, + ) + .map_err(|e| anyhow!("build_vote_commitment: {}", e))?; + + json_to_jstring(env, &JsonVoteCommitmentBundle::from(&bundle)) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +// ============================================================================= +// J. Share payloads +// ============================================================================= + +/// Build share payloads for distribution to tally-server helpers. +/// +/// [encSharesJson] JSON array of WireEncryptedShare (c1/c2/share_index) +/// extracted from the VoteCommitmentBundle.enc_shares field. +/// [commitmentJson] Full JSON-encoded VoteCommitmentBundle. +/// [vcTreePosition] Position of the vote commitment leaf in the VC tree +/// (known after the cast-vote TX is confirmed on chain). +/// +/// Returns JSON array of SharePayload. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_buildSharePayloadsJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + enc_shares_json: JString<'local>, + commitment_json: JString<'local>, + vote_decision: jint, + num_options: jint, + vc_tree_position: jlong, + single_share_mode: jboolean, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + // Deserialize enc_shares (wire-safe public components) + let json_shares: Vec = + json_from_jstring(env, &enc_shares_json, "encSharesJson")?; + let enc_shares: Vec = json_shares + .into_iter() + .map(WireEncryptedShare::try_from) + .collect::>()?; + + // Deserialize VoteCommitmentBundle from JSON (reconstruct wire-safe version) + let json_bundle: JsonVoteCommitmentBundle = + json_from_jstring(env, &commitment_json, "commitmentJson")?; + let commitment = VoteCommitmentBundle { + van_nullifier: hex_dec(&json_bundle.van_nullifier, "van_nullifier")?, + vote_authority_note_new: hex_dec( + &json_bundle.vote_authority_note_new, + "vote_authority_note_new", + )?, + vote_commitment: hex_dec(&json_bundle.vote_commitment, "vote_commitment")?, + proposal_id: json_bundle.proposal_id, + proof: hex_dec(&json_bundle.proof, "proof")?, + enc_shares: Vec::new(), // wire-only path — not used by build_share_payloads + anchor_height: json_bundle.anchor_height, + vote_round_id: json_bundle.vote_round_id, + shares_hash: hex_dec(&json_bundle.shares_hash, "shares_hash")?, + share_blinds: json_bundle + .share_blinds + .iter() + .enumerate() + .map(|(i, h)| hex_dec(h, &format!("share_blinds[{i}]"))) + .collect::>()?, + share_comms: json_bundle + .share_comms + .iter() + .enumerate() + .map(|(i, h)| hex_dec(h, &format!("share_comms[{i}]"))) + .collect::>()?, + r_vpk_bytes: hex_dec(&json_bundle.r_vpk_bytes, "r_vpk_bytes")?, + alpha_v: hex_dec(&json_bundle.alpha_v, "alpha_v")?, + }; + + // Note: build_share_payloads is a pure function (no VotingDb needed). + let payloads: Vec = voting::vote_commitment::build_share_payloads( + &enc_shares, + &commitment, + vote_decision as u32, + num_options as u32, + vc_tree_position as u64, + single_share_mode == JNI_TRUE, + ) + .map_err(|e| anyhow!("build_share_payloads: {}", e))? + .into_iter() + .map(JsonSharePayload::from) + .collect(); + + json_to_jstring(env, &payloads) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +// ============================================================================= +// K. Cast vote signature +// ============================================================================= + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_signCastVote< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + hotkey_seed: JByteArray<'local>, + network_id: jint, + round_id: JString<'local>, + r_vpk: JByteArray<'local>, + van_nullifier: JByteArray<'local>, + van_new: JByteArray<'local>, + vote_commitment: JByteArray<'local>, + proposal_id: jint, + anchor_height: jint, + alpha_v: JByteArray<'local>, +) -> jbyteArray { + let res = catch_unwind(&mut env, |env| { + let sig = voting::vote_commitment::sign_cast_vote( + &env.convert_byte_array(&hotkey_seed)?, + network_id as u32, + &java_string_to_rust(env, &round_id)?, + &env.convert_byte_array(&r_vpk)?, + &env.convert_byte_array(&van_nullifier)?, + &env.convert_byte_array(&van_new)?, + &env.convert_byte_array(&vote_commitment)?, + proposal_id as u32, + anchor_height as u32, + &env.convert_byte_array(&alpha_v)?, + ) + .map_err(|e| anyhow!("sign_cast_vote: {}", e))?; + Ok(env.byte_array_from_slice(&sig.vote_auth_sig)?.into_raw()) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +// ============================================================================= +// L. Delegation submission +// ============================================================================= + +/// Reconstruct the chain-ready delegation TX payload from the DB + wallet seed. +/// +/// Loads proof artifacts and signs the ZIP-244 sighash with the randomised hotkey spending key. +/// Returns JSON-encoded DelegationSubmissionData. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_getDelegationSubmissionJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + bundle_index: jint, + sender_seed: JByteArray<'local>, + network_id: jint, + account_index: jint, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let data = handle + .db + .get_delegation_submission( + &java_string_to_rust(env, &round_id)?, + bundle_index as u32, + &env.convert_byte_array(&sender_seed)?, + network_id as u32, + account_index as u32, + ) + .map_err(|e| anyhow!("get_delegation_submission: {}", e))?; + json_to_jstring(env, &JsonDelegationSubmission::from(data)) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +/// Reconstruct the delegation TX payload using a Keystone-provided signature. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_getDelegationSubmissionWithKeystoneSigJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + bundle_index: jint, + keystone_sig: JByteArray<'local>, + keystone_sighash: JByteArray<'local>, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let data = handle + .db + .get_delegation_submission_with_keystone_sig( + &java_string_to_rust(env, &round_id)?, + bundle_index as u32, + &env.convert_byte_array(&keystone_sig)?, + &env.convert_byte_array(&keystone_sighash)?, + ) + .map_err(|e| anyhow!("get_delegation_submission_with_keystone_sig: {}", e))?; + json_to_jstring(env, &JsonDelegationSubmission::from(data)) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +// ============================================================================= +// M. Stateless utilities +// ============================================================================= + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_decomposeWeightJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + weight: jlong, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let parts = voting::decompose::decompose_weight(weight as u64); + json_to_jstring(env, &parts) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_extractOrchardFvkFromUfvk< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + ufvk: JString<'local>, + network_id: jint, +) -> jbyteArray { + let res = catch_unwind(&mut env, |env| { + let network = network_from_id(network_id)?; + let bytes = orchard_fvk_bytes(&java_string_to_rust(env, &ufvk)?, network)?; + Ok(env.byte_array_from_slice(&bytes)?.into_raw()) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +// ============================================================================= +// JNI object constructors (for Kotlin data classes with typed fields) +// ============================================================================= + +fn make_ffi_round_state<'local>( + env: &mut JNIEnv<'local>, + state: RoundState, +) -> anyhow::Result { + let phase = round_phase_to_u32(state.phase) as i32; + let class = + env.find_class("cash/z/ecc/android/sdk/internal/model/voting/FfiRoundState")?; + let round_id_obj: JObject<'local> = env.new_string(&state.round_id)?.into(); + let hotkey_obj: JObject<'local> = match &state.hotkey_address { + Some(a) => env.new_string(a)?.into(), + None => JObject::null(), + }; + let long_class = env.find_class("java/lang/Long")?; + let weight_obj: JObject<'local> = match state.delegated_weight { + Some(w) => env.new_object(&long_class, "(J)V", &[JValue::Long(w as i64)])?, + None => JObject::null(), + }; + let obj = env.new_object( + &class, + "(Ljava/lang/String;IJLjava/lang/String;Ljava/lang/Long;Z)V", + &[ + JValue::Object(&round_id_obj), + JValue::Int(phase), + JValue::Long(state.snapshot_height as i64), + JValue::Object(&hotkey_obj), + JValue::Object(&weight_obj), + JValue::Bool(state.proof_generated as jboolean), + ], + )?; + Ok(obj.into_raw()) +} + +fn make_ffi_voting_hotkey<'local>( + env: &mut JNIEnv<'local>, + hotkey: voting::types::VotingHotkey, +) -> anyhow::Result { + let class = + env.find_class("cash/z/ecc/android/sdk/internal/model/voting/FfiVotingHotkey")?; + let sk_obj: JObject<'local> = env.byte_array_from_slice(&hotkey.secret_key)?.into(); + let pk_obj: JObject<'local> = env.byte_array_from_slice(&hotkey.public_key)?.into(); + let addr_obj: JObject<'local> = env.new_string(&hotkey.address)?.into(); + let obj = env.new_object( + &class, + "([B[BLjava/lang/String;)V", + &[ + JValue::Object(&sk_obj), + JValue::Object(&pk_obj), + JValue::Object(&addr_obj), + ], + )?; + Ok(obj.into_raw()) +} + +fn make_ffi_bundle_setup_result<'local>( + env: &mut JNIEnv<'local>, + count: u32, + weight: u64, +) -> anyhow::Result { + let class = + env.find_class("cash/z/ecc/android/sdk/internal/model/voting/FfiBundleSetupResult")?; + let obj = env.new_object( + &class, + "(IJ)V", + &[JValue::Int(count as i32), JValue::Long(weight as i64)], + )?; + Ok(obj.into_raw()) +} + +/// Convert a ReceivedNote<_, orchard::note::Note> to a JsonNoteInfo. +fn received_note_to_note_info( + note: &zcash_client_backend::wallet::ReceivedNote< + zcash_client_sqlite::ReceivedNoteId, + orchard::note::Note, + >, + ufvk: &UnifiedFullViewingKey, + network: &zcash_protocol::consensus::Network, +) -> anyhow::Result { + use orchard::keys::Scope; + + let orchard_note = note.note(); + let fvk = ufvk + .orchard() + .ok_or_else(|| anyhow!("UFVK has no Orchard component"))?; + + let nullifier = orchard_note.nullifier(fvk); + let cmx: orchard::note::ExtractedNoteCommitment = orchard_note.commitment().into(); + + let diversifier = orchard_note.recipient().diversifier().as_array().to_vec(); + let value = orchard_note.value().inner(); + let rho = orchard_note.rho().to_bytes().to_vec(); + let rseed = orchard_note.rseed().as_bytes().to_vec(); + let position = u64::from(note.note_commitment_tree_position()); + let scope = match note.spending_key_scope() { + Scope::External => 0u32, + Scope::Internal => 1u32, + }; + let ufvk_str = ufvk.encode(network); + + Ok(JsonNoteInfo { + commitment: hex_enc(&cmx.to_bytes()), + nullifier: hex_enc(&nullifier.to_bytes()), + value, + position, + diversifier: hex_enc(&diversifier), + rho: hex_enc(&rho), + rseed: hex_enc(&rseed), + scope, + ufvk_str, + }) +} + +/// Caches the lightwalletd TreeState protobuf for the snapshot height. +/// Must be called before [generateNoteWitnessesJson]. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_storeTreeState< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + tree_state_bytes: JByteArray<'local>, +) -> jboolean { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let bytes = env.convert_byte_array(&tree_state_bytes)?; + handle + .db + .store_tree_state(&java_string_to_rust(env, &round_id)?, &bytes) + .map_err(|e| anyhow!("store_tree_state: {}", e))?; + Ok(JNI_TRUE) + }); + unwrap_exc_or(&mut env, res, JNI_FALSE) +} + +/// Returns JSON array of NoteInfo for unspent Orchard notes at [snapshotHeight]. +/// +/// Opens the wallet SQLite at [walletDbPath] read-only and calls +/// `get_unspent_orchard_notes_at_historical_height`. The [accountUuidBytes] +/// parameter must be exactly 16 bytes (UUID). +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_getWalletNotesJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + wallet_db_path: JString<'local>, + snapshot_height: jlong, + network_id: jint, + account_uuid_bytes: JByteArray<'local>, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + use zcash_client_backend::data_api::{Account, WalletRead}; + use zcash_protocol::consensus::BlockHeight; + + let path = java_string_to_rust(env, &wallet_db_path)?; + let network = network_from_id(network_id)?; + let height = BlockHeight::from_u32(snapshot_height as u32); + + let uuid_bytes = env.convert_byte_array(&account_uuid_bytes)?; + let uuid_arr: [u8; 16] = uuid_bytes + .try_into() + .map_err(|_| anyhow!("accountUuidBytes must be 16 bytes"))?; + let account_uuid = + zcash_client_sqlite::AccountUuid::from_uuid(uuid::Uuid::from_bytes(uuid_arr)); + + let wallet_db = zcash_client_sqlite::WalletDb::for_path( + &path, + network, + zcash_client_sqlite::util::SystemClock, + rand::rngs::OsRng, + ) + .map_err(|e| anyhow!("failed to open wallet DB: {}", e))?; + + let account = wallet_db + .get_account(account_uuid) + .map_err(|e| anyhow!("get_account: {}", e))? + .ok_or_else(|| anyhow!("account not found in wallet DB"))?; + let ufvk = account + .ufvk() + .ok_or_else(|| anyhow!("account has no UFVK"))? + .clone(); + + let notes = wallet_db + .get_unspent_orchard_notes_at_historical_height(account_uuid, height) + .map_err(|e| anyhow!("get_unspent_orchard_notes_at_historical_height: {}", e))?; + + let json_notes: Vec = notes + .iter() + .map(|rn| received_note_to_note_info(rn, &ufvk, &network)) + .collect::>()?; + + json_to_jstring(env, &json_notes) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +/// Generates Orchard Merkle witnesses for notes in a bundle and caches them in the voting DB. +/// +/// Requires [storeTreeState] to have been called first (loads the frontier from the +/// cached tree state). Also calls [storeWitnesses] internally. +/// +/// [notesJson] is the JSON array of NoteInfo from [getWalletNotesJson] for this bundle. +/// Returns JSON array of WitnessData, or null on error. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_generateNoteWitnessesJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + bundle_index: jint, + wallet_db_path: JString<'local>, + notes_json: JString<'local>, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + use incrementalmerkletree::Position; + use orchard::tree::MerkleHashOrchard; + use prost::Message; + use zcash_client_backend::proto::service::TreeState; + use zcash_protocol::consensus::BlockHeight; + + let handle = handle_from_jlong(db_handle)?; + let round_id_str = java_string_to_rust(env, &round_id)?; + let wallet_path = java_string_to_rust(env, &wallet_db_path)?; + + let json_notes: Vec = + json_from_jstring(env, ¬es_json, "notesJson")?; + let core_notes: Vec = json_notes + .into_iter() + .map(NoteInfo::try_from) + .collect::>()?; + + // Load tree state and params from voting DB + let conn = handle.db.conn(); + let wallet_id = handle.db.wallet_id(); + let tree_state_bytes = + voting::storage::queries::load_tree_state(&conn, &round_id_str, &wallet_id) + .map_err(|e| anyhow!("load_tree_state: {}", e))?; + let params = + voting::storage::queries::load_round_params(&conn, &round_id_str, &wallet_id) + .map_err(|e| anyhow!("load_round_params: {}", e))?; + drop(conn); + + // Parse frontier from cached TreeState + let tree_state = TreeState::decode(tree_state_bytes.as_slice()) + .map_err(|e| anyhow!("decode TreeState: {}", e))?; + let orchard_ct = tree_state + .orchard_tree() + .map_err(|e| anyhow!("parse orchard_tree: {}", e))?; + let frontier_root = orchard_ct.root(); + let nonempty_frontier = orchard_ct + .to_frontier() + .take() + .ok_or_else(|| anyhow!("empty orchard frontier — no Orchard activity at snapshot"))?; + + let height = + BlockHeight::from_u32(params.snapshot_height as u32); + + // Open wallet DB and generate witnesses + let wallet_db = zcash_client_sqlite::WalletDb::for_path( + &wallet_path, + zcash_protocol::consensus::Network::MainNetwork, // network not used for tree ops + zcash_client_sqlite::util::SystemClock, + rand::rngs::OsRng, + ) + .map_err(|e| anyhow!("open wallet DB: {}", e))?; + + let positions: Vec = core_notes + .iter() + .map(|n| Position::from(n.position)) + .collect(); + + let merkle_paths = wallet_db + .generate_orchard_witnesses_at_historical_height( + &positions, + nonempty_frontier, + height, + ) + .map_err(|e| anyhow!("generate_orchard_witnesses_at_historical_height: {}", e))?; + + let root_bytes = frontier_root.to_bytes().to_vec(); + let witnesses: Vec = merkle_paths + .into_iter() + .zip(core_notes.iter()) + .map(|(path, note)| { + let auth_path: Vec> = path + .path_elems() + .iter() + .map(|h: &MerkleHashOrchard| h.to_bytes().to_vec()) + .collect(); + WitnessData { + note_commitment: note.commitment.clone(), + position: note.position, + root: root_bytes.clone(), + auth_path, + } + }) + .collect(); + + // Cache in voting DB + handle + .db + .store_witnesses(&round_id_str, bundle_index as u32, &witnesses) + .map_err(|e| anyhow!("store_witnesses: {}", e))?; + + let json_witnesses: Vec = + witnesses.into_iter().map(JsonWitnessData::from).collect(); + json_to_jstring(env, &json_witnesses) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index b70a84381..0321ac626 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -85,6 +85,7 @@ import cash.z.ecc.android.sdk.type.ServerValidation import cash.z.ecc.android.sdk.util.WalletClientFactory import co.electriccoin.lightwallet.client.CombinedWalletClient import co.electriccoin.lightwallet.client.ServiceMode +import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe import co.electriccoin.lightwallet.client.model.LightWalletEndpoint import co.electriccoin.lightwallet.client.model.Response import co.electriccoin.lightwallet.client.util.use @@ -98,6 +99,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel +import kotlinx.coroutines.withContext import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -386,6 +388,19 @@ class SdkSynchronizer private constructor( */ override val networkHeight: StateFlow = processor.networkHeight + override val fullyScannedHeight: StateFlow = + processor.processorInfo + .map { + runCatching { backend.getFullyScannedHeight() } + .onFailure { Twig.error(it) { "Failed to get fully scanned height" } } + .getOrNull() + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + // // Error Handling // @@ -680,6 +695,32 @@ class SdkSynchronizer private constructor( refreshAccountsBus.emit(Unit) } + override suspend fun getTreeState(height: BlockHeight): ByteArray { + return when ( + val response = + processor.downloader.getTreeState( + height = BlockHeightUnsafe(height.value), + serviceMode = sdkFlags ifTor ServiceMode.UniqueTor + ) + ) { + is Response.Success -> TreeState.new(response.result).encoded + is Response.Failure -> { + val message = "Failed to fetch tree state at height ${height.value}: ${response.toThrowable()}" + Twig.error { message } + throw response.toThrowable() + } + } + } + + override suspend fun getWalletDbPath(): String = + withContext(Dispatchers.IO) { + DatabaseCoordinator.getInstance(context) + .dataDbFile( + network = synchronizerKey.zcashNetwork, + alias = synchronizerKey.alias + ).absolutePath + } + suspend fun isValidAddress(address: String): Boolean = !validateAddress(address).isNotValid // diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt index b934b9fac..2c63bdad8 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt @@ -101,6 +101,15 @@ interface Synchronizer { */ val networkHeight: StateFlow + /** + * The height to which the wallet has been fully scanned. + * + * This is the height for which the wallet has fully trial-decrypted this and all preceding + * blocks above the wallet's birthday height. This value is useful for determining whether the + * wallet has scanned up to a specific snapshot height (e.g., for coinholder voting eligibility). + */ + val fullyScannedHeight: StateFlow + /** * A stream of wallet balances */ @@ -666,6 +675,23 @@ interface Synchronizer { suspend fun deleteAccount(accountUuid: AccountUuid): Boolean + /** + * Returns the commitment tree state at the given block height, encoded as a protobuf [ByteArray]. + * + * This is used by the coinholder voting protocol to generate witnesses and verify inclusion + * proofs against the Orchard note commitment tree at a specific snapshot height. + * + * @param height the block height at which to fetch the tree state + * @return the serialized tree state bytes + */ + suspend fun getTreeState(height: BlockHeight): ByteArray + + /** + * Returns the absolute path to the wallet's SQLite database file. + * Used by the voting backend to read notes at historical heights. + */ + suspend fun getWalletDbPath(): String + // // Error Handling // diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackend.kt new file mode 100644 index 000000000..124af8c13 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackend.kt @@ -0,0 +1,393 @@ +package cash.z.ecc.android.sdk.internal + +import cash.z.ecc.android.sdk.internal.model.voting.FfiBundleSetupResult +import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState +import cash.z.ecc.android.sdk.internal.model.voting.FfiVotingHotkey + +/** + * Type-safe orchestration layer for shielded voting operations. + * + * Sits between [SdkSynchronizer] and [VotingRustBackend], providing suspend functions + * with proper Kotlin types and encapsulating the multi-step voting protocol. + * + * Caller responsibilities (managed by the app/VM layer, not this interface): + * - Fetching NoteInfo JSON from the wallet at the snapshot height. + * - Securely storing [FfiVotingHotkey.secretKey] between protocol steps. + * - Providing the 32-byte wallet seed fingerprint (ZIP-32 SeedFingerprint). + * - Confirming the delegation TX on chain and providing the VAN tree position. + */ +@Suppress("TooManyFunctions") +interface TypesafeVotingBackend { + + // ─── Database lifecycle ──────────────────────────────────────────────────── + + suspend fun openVotingDb(dbPath: String): Long + suspend fun closeVotingDb(dbHandle: Long) + suspend fun setWalletId(dbHandle: Long, walletId: String) + + // ─── Round management ───────────────────────────────────────────────────── + + suspend fun initRound( + dbHandle: Long, + roundId: String, + snapshotHeight: Long, + eaPK: ByteArray, + ncRoot: ByteArray, + nullifierIMTRoot: ByteArray, + sessionJson: String? + ) + + suspend fun getRoundState(dbHandle: Long, roundId: String): FfiRoundState? + + /** Returns JSON array of RoundSummary objects from the voting database. */ + suspend fun listRoundsJson(dbHandle: Long): String + + suspend fun clearRound(dbHandle: Long, roundId: String) + + // ─── Note setup ─────────────────────────────────────────────────────────── + + /** + * Splits [notesJson] (JSON array of NoteInfo at snapshotHeight) into voting bundles. + * + * NoteInfo fields (all hex-encoded bytes): + * ```json + * [{ "commitment":"hex", "nullifier":"hex", "value":12500000, + * "position":42, "diversifier":"hex", "rho":"hex", "rseed":"hex", + * "scope":0, "ufvk_str":"uviewtest1..." }, ...] + * ``` + */ + suspend fun setupBundles( + dbHandle: Long, + roundId: String, + notesJson: String + ): FfiBundleSetupResult + + // ─── Hotkey generation ──────────────────────────────────────────────────── + + /** + * Generates a per-round Pallas voting hotkey from [seed] (≥ 32 random bytes). + * Caller must store [FfiVotingHotkey.secretKey] securely (e.g. encrypted storage). + */ + suspend fun generateHotkey( + dbHandle: Long, + roundId: String, + seed: ByteArray + ): FfiVotingHotkey + + // ─── Witnesses (required before buildAndProveDelegation) ───────────────── + + /** + * Caches Merkle witnesses for all notes in a bundle. + * Must be called before [buildAndProveDelegation]. + * + * [witnessesJson] JSON array of WitnessData (one per note): + * ```json + * [{ "note_commitment":"hex", "position":42, "root":"hex", + * "auth_path":["hex32",...] }, ...] + * ``` + */ + suspend fun storeWitnesses( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + witnessesJson: String + ) + + // ─── Governance PCZT (Keystone signing flow) ────────────────────────────── + + /** + * Builds the governance PCZT for [bundleIndex]. + * + * @param notesJson JSON array of NoteInfo for this bundle (same as [setupBundles]) + * @param hotkeyRawSeed 32-byte hotkey seed — used to derive the Orchard hotkey address + * @param seedFingerprint 32-byte ZIP-32 seed fingerprint of the wallet (SeedFingerprint.to_bytes()) + * @param roundName Human-readable round name from the session data + * @param addressIndex ZIP-32 address index for the governance PCZT output (normally 0) + */ + suspend fun buildGovernancePczt( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + ufvk: String, + networkId: Int, + accountIndex: Int, + notesJson: String, + hotkeyRawSeed: ByteArray, + seedFingerprint: ByteArray, + roundName: String, + addressIndex: Int = 0 + ): GovernancePcztResult + + // ─── Delegation proof (ZKP1) ────────────────────────────────────────────── + + /** + * Generates the Halo2 delegation proof. Long-running (10s–several minutes). + * + * @param notesJson JSON array of NoteInfo for this bundle (same as [storeWitnesses]) + * @param hotkeyRawSeed 32-byte hotkey seed (Orchard hotkey address is derived from this) + */ + suspend fun buildAndProveDelegation( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + pirServerUrl: String, + networkId: Int, + notesJson: String, + hotkeyRawSeed: ByteArray + ): DelegationProofResult + + // ─── Delegation submission ───────────────────────────────────────────────── + + /** + * Reconstructs the chain-ready delegation TX payload using the wallet spending key. + * Call after [buildAndProveDelegation] completes. + * + * @param senderSeed Wallet seed (used to sign the ZIP-244 sighash) + * @param accountIndex ZIP-32 account index + */ + suspend fun getDelegationSubmission( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + senderSeed: ByteArray, + networkId: Int, + accountIndex: Int + ): DelegationSubmissionResult + + /** + * Reconstructs the delegation TX payload using a Keystone-provided signature. + * Call instead of [getDelegationSubmission] for Keystone hardware wallet flow. + */ + suspend fun getDelegationSubmissionWithKeystoneSig( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + keystoneSig: ByteArray, + keystoneSighash: ByteArray + ): DelegationSubmissionResult + + // ─── Vote commitment tree ───────────────────────────────────────────────── + + /** + * Syncs the vote commitment tree from the chain node. + * Returns the latest synced block height, or -1 on error. + */ + suspend fun syncVoteTree( + dbHandle: Long, + roundId: String, + nodeUrl: String + ): Long + + /** + * Stores the VAN leaf position after the delegation TX is confirmed on chain. + * Must be called before [generateVanWitnessJson]. + * + * @param position VAN leaf position reported in the delegation TX response events + */ + suspend fun storeVanPosition( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + position: Int + ) + + /** + * Generates the Merkle witness for the VAN note (input to ZKP2). + * Requires [syncVoteTree] + [storeVanPosition] to have been called first. + * + * Returns JSON-encoded VanWitness: + * ```json + * { "auth_path":["hex32",...], "position":42, "anchor_height":1234 } + * ``` + */ + suspend fun generateVanWitnessJson( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + anchorHeight: Int + ): String + + // ─── Tree state + note witnesses ───────────────────────────────────────── + + /** Caches the lightwalletd TreeState for the snapshot height (required before [generateNoteWitnesses]). */ + suspend fun storeTreeState(dbHandle: Long, roundId: String, treeStateBytes: ByteArray) + + /** + * Returns NoteInfo JSON array for unspent Orchard notes at [snapshotHeight]. + * [accountUuidBytes] must be 16 bytes (UUID). + */ + suspend fun getWalletNotes( + walletDbPath: String, + snapshotHeight: Long, + networkId: Int, + accountUuidBytes: ByteArray + ): String + + /** + * Generates Orchard witnesses for a bundle and caches them in the voting DB. + * Requires [storeTreeState] first. Returns JSON array of WitnessData. + */ + suspend fun generateNoteWitnesses( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + walletDbPath: String, + notesJson: String + ): String + + // ─── Vote commitment (ZKP2) ──────────────────────────────────────────────── + + /** + * Builds the vote commitment proof for one proposal choice. + * + * @param witnessJson JSON VanWitness from [generateVanWitnessJson] + * @param singleShare true for single-share mode (test / dev only) + * + * The returned [VoteCommitmentResult] includes the raw JSON needed for + * [buildSharePayloadsJson] so the caller doesn't need to re-serialise. + */ + suspend fun buildVoteCommitment( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + hotkeySeed: ByteArray, + proposalId: Int, + choice: Int, + numOptions: Int, + witnessJson: String, + vanPosition: Int, + anchorHeight: Int, + networkId: Int, + singleShare: Boolean = false + ): VoteCommitmentResult + + // ─── Share payloads ─────────────────────────────────────────────────────── + + /** + * Builds share payloads for distribution to vote-server helpers. + * + * Typical call pattern using [VoteCommitmentResult]: + * ```kotlin + * val payloadsJson = buildSharePayloadsJson( + * encSharesJson = commitment.encSharesJson, + * commitmentJson = commitment.rawBundleJson, + * voteDecision = choice, + * numOptions = numOptions, + * vcTreePosition = confirmedVcPosition, + * singleShareMode = false + * ) + * ``` + * + * Returns JSON array of SharePayload ready for [VotingApiProvider.submitVoteCommitment]. + */ + suspend fun buildSharePayloadsJson( + encSharesJson: String, + commitmentJson: String, + voteDecision: Int, + numOptions: Int, + vcTreePosition: Long, + singleShareMode: Boolean = false + ): String + + // ─── Cast vote signature ─────────────────────────────────────────────────── + + suspend fun signCastVote( + hotkeySeed: ByteArray, + networkId: Int, + roundId: String, + rVpk: ByteArray, + vanNullifier: ByteArray, + vanNew: ByteArray, + voteCommitment: ByteArray, + proposalId: Int, + anchorHeight: Int, + alphaV: ByteArray + ): ByteArray + + // ─── Stateless utilities ────────────────────────────────────────────────── + + suspend fun decomposeWeight(weight: Long): List + suspend fun extractOrchardFvkFromUfvk(ufvk: String, networkId: Int): ByteArray +} + +// ============================================================================= +// Result types +// ============================================================================= + +data class GovernancePcztResult( + /** Raw PCZT bytes for display as QR code (Keystone) or direct signing. */ + val pcztBytes: ByteArray, + /** ZIP-244 sighash — used to verify the Keystone signature. */ + val sighash: ByteArray, + /** Index of the governance action within the PCZT. */ + val actionIndex: Int +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GovernancePcztResult) return false + return sighash.contentEquals(other.sighash) + } + + override fun hashCode(): Int = sighash.contentHashCode() +} + +data class DelegationProofResult( + val proof: ByteArray, + val publicInputs: List, + val nfSigned: ByteArray, + val cmxNew: ByteArray, + val govNullifiers: List, + val vanComm: ByteArray, + val rk: ByteArray +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DelegationProofResult) return false + return proof.contentEquals(other.proof) + } + + override fun hashCode(): Int = proof.contentHashCode() +} + +data class VoteCommitmentResult( + /** Parsed fields needed for [TypesafeVotingBackend.signCastVote]. */ + val vanNullifier: ByteArray, + val voteAuthorityNoteNew: ByteArray, + val voteCommitment: ByteArray, + val rVpk: ByteArray, + val alphaV: ByteArray, + val anchorHeight: Int, + /** Wire-safe encrypted shares JSON — pass to [TypesafeVotingBackend.buildSharePayloadsJson]. */ + val encSharesJson: String, + /** Full bundle JSON — pass as [commitmentJson] to [TypesafeVotingBackend.buildSharePayloadsJson]. */ + val rawBundleJson: String +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is VoteCommitmentResult) return false + return voteCommitment.contentEquals(other.voteCommitment) + } + + override fun hashCode(): Int = voteCommitment.contentHashCode() +} + +data class DelegationSubmissionResult( + val proof: ByteArray, + val rk: ByteArray, + val spendAuthSig: ByteArray, + val sighash: ByteArray, + val nfSigned: ByteArray, + val cmxNew: ByteArray, + val govComm: ByteArray, + val govNullifiers: List, + val alpha: ByteArray, + val voteRoundId: String +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DelegationSubmissionResult) return false + return sighash.contentEquals(other.sighash) + } + + override fun hashCode(): Int = sighash.contentHashCode() +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackendImpl.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackendImpl.kt new file mode 100644 index 000000000..c72b43685 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackendImpl.kt @@ -0,0 +1,377 @@ +package cash.z.ecc.android.sdk.internal + +import cash.z.ecc.android.sdk.internal.jni.VotingRustBackend +import cash.z.ecc.android.sdk.internal.model.voting.FfiBundleSetupResult +import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState +import cash.z.ecc.android.sdk.internal.model.voting.FfiVotingHotkey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Concrete implementation of [TypesafeVotingBackend] that delegates to [VotingRustBackend] JNI. + * + * All operations dispatch to [Dispatchers.IO] since they touch SQLite or perform + * long-running ZK proof generation. + */ +class TypesafeVotingBackendImpl : TypesafeVotingBackend { + + // ─── Database lifecycle ──────────────────────────────────────────────────── + + override suspend fun openVotingDb(dbPath: String): Long = + io { VotingRustBackend.openVotingDb(dbPath) } + + override suspend fun closeVotingDb(dbHandle: Long) = + io { VotingRustBackend.closeVotingDb(dbHandle) } + + override suspend fun setWalletId(dbHandle: Long, walletId: String) = + io { VotingRustBackend.setWalletId(dbHandle, walletId); Unit } + + // ─── Round management ───────────────────────────────────────────────────── + + override suspend fun initRound( + dbHandle: Long, + roundId: String, + snapshotHeight: Long, + eaPK: ByteArray, + ncRoot: ByteArray, + nullifierIMTRoot: ByteArray, + sessionJson: String? + ) = io { + check(VotingRustBackend.initRound(dbHandle, roundId, snapshotHeight, eaPK, ncRoot, nullifierIMTRoot, sessionJson)) { + "initRound failed for roundId=$roundId" + } + } + + override suspend fun getRoundState(dbHandle: Long, roundId: String): FfiRoundState? = + io { VotingRustBackend.getRoundState(dbHandle, roundId) } + + override suspend fun listRoundsJson(dbHandle: Long): String = + io { VotingRustBackend.listRoundsJson(dbHandle) } + + override suspend fun clearRound(dbHandle: Long, roundId: String) = + io { + check(VotingRustBackend.clearRound(dbHandle, roundId)) { + "clearRound failed for roundId=$roundId" + } + } + + // ─── Note setup ─────────────────────────────────────────────────────────── + + override suspend fun setupBundles( + dbHandle: Long, + roundId: String, + notesJson: String + ): FfiBundleSetupResult = + io { + VotingRustBackend.setupBundles(dbHandle, roundId, notesJson) + ?: error("setupBundles returned null for roundId=$roundId") + } + + // ─── Hotkey ─────────────────────────────────────────────────────────────── + + override suspend fun generateHotkey( + dbHandle: Long, + roundId: String, + seed: ByteArray + ): FfiVotingHotkey = + io { + VotingRustBackend.generateHotkey(dbHandle, roundId, seed) + ?: error("generateHotkey returned null for roundId=$roundId") + } + + // ─── Witnesses ──────────────────────────────────────────────────────────── + + override suspend fun storeWitnesses( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + witnessesJson: String + ) = io { + check(VotingRustBackend.storeWitnesses(dbHandle, roundId, bundleIndex, witnessesJson)) { + "storeWitnesses failed for roundId=$roundId bundle=$bundleIndex" + } + } + + // ─── Governance PCZT ───────────────────────────────────────────────────── + + override suspend fun buildGovernancePczt( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + ufvk: String, + networkId: Int, + accountIndex: Int, + notesJson: String, + hotkeyRawSeed: ByteArray, + seedFingerprint: ByteArray, + roundName: String, + addressIndex: Int + ): GovernancePcztResult = + io { + val json = VotingRustBackend.buildGovernancePcztJson( + dbHandle, roundId, bundleIndex, ufvk, networkId, accountIndex, + notesJson, hotkeyRawSeed, seedFingerprint, roundName, addressIndex + ) ?: error("buildGovernancePczt returned null") + + val obj = org.json.JSONObject(json) + GovernancePcztResult( + pcztBytes = hexDec(obj.getString("pczt_bytes")), + sighash = hexDec(obj.getString("pczt_sighash")), + actionIndex = obj.getInt("action_index") + ) + } + + // extractPcztSighash is a lower-level utility exposed directly via VotingRustBackend + // and not part of the TypesafeVotingBackend interface. + suspend fun extractPcztSighash(pcztBytes: ByteArray): ByteArray = + io { + VotingRustBackend.extractPcztSighash(pcztBytes) + ?: error("extractPcztSighash returned null") + } + + // ─── Tree state + note witnesses ───────────────────────────────────────── + + override suspend fun storeTreeState( + dbHandle: Long, + roundId: String, + treeStateBytes: ByteArray + ) = io { + check(VotingRustBackend.storeTreeState(dbHandle, roundId, treeStateBytes)) { + "storeTreeState failed for roundId=$roundId" + } + } + + override suspend fun getWalletNotes( + walletDbPath: String, + snapshotHeight: Long, + networkId: Int, + accountUuidBytes: ByteArray + ): String = + io { + VotingRustBackend.getWalletNotesJson(walletDbPath, snapshotHeight, networkId, accountUuidBytes) + ?: error("getWalletNotes returned null") + } + + override suspend fun generateNoteWitnesses( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + walletDbPath: String, + notesJson: String + ): String = + io { + VotingRustBackend.generateNoteWitnessesJson(dbHandle, roundId, bundleIndex, walletDbPath, notesJson) + ?: error("generateNoteWitnesses returned null for bundle=$bundleIndex") + } + + // ─── Delegation proof ───────────────────────────────────────────────────── + + override suspend fun buildAndProveDelegation( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + pirServerUrl: String, + networkId: Int, + notesJson: String, + hotkeyRawSeed: ByteArray + ): DelegationProofResult = + io { + val json = VotingRustBackend.buildAndProveDelegationJson( + dbHandle, roundId, bundleIndex, pirServerUrl, networkId, notesJson, hotkeyRawSeed + ) ?: error("buildAndProveDelegation returned null") + + val obj = org.json.JSONObject(json) + val govNullifiers = obj.getJSONArray("gov_nullifiers") + .let { arr -> (0 until arr.length()).map { hexDec(arr.getString(it)) } } + val publicInputs = obj.getJSONArray("public_inputs") + .let { arr -> (0 until arr.length()).map { hexDec(arr.getString(it)) } } + + DelegationProofResult( + proof = hexDec(obj.getString("proof")), + publicInputs = publicInputs, + nfSigned = hexDec(obj.getString("nf_signed")), + cmxNew = hexDec(obj.getString("cmx_new")), + govNullifiers = govNullifiers, + vanComm = hexDec(obj.getString("van_comm")), + rk = hexDec(obj.getString("rk")) + ) + } + + // ─── Delegation submission ───────────────────────────────────────────────── + + override suspend fun getDelegationSubmission( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + senderSeed: ByteArray, + networkId: Int, + accountIndex: Int + ): DelegationSubmissionResult = + io { + val json = VotingRustBackend.getDelegationSubmissionJson( + dbHandle, roundId, bundleIndex, senderSeed, networkId, accountIndex + ) ?: error("getDelegationSubmission returned null") + parseDelegationSubmission(json) + } + + override suspend fun getDelegationSubmissionWithKeystoneSig( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + keystoneSig: ByteArray, + keystoneSighash: ByteArray + ): DelegationSubmissionResult = + io { + val json = VotingRustBackend.getDelegationSubmissionWithKeystoneSigJson( + dbHandle, roundId, bundleIndex, keystoneSig, keystoneSighash + ) ?: error("getDelegationSubmissionWithKeystoneSig returned null") + parseDelegationSubmission(json) + } + + // ─── Vote commitment tree ───────────────────────────────────────────────── + + override suspend fun syncVoteTree( + dbHandle: Long, + roundId: String, + nodeUrl: String + ): Long = io { VotingRustBackend.syncVoteTree(dbHandle, roundId, nodeUrl) } + + override suspend fun storeVanPosition( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + position: Int + ) = io { + check(VotingRustBackend.storeVanPosition(dbHandle, roundId, bundleIndex, position)) { + "storeVanPosition failed" + } + } + + override suspend fun generateVanWitnessJson( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + anchorHeight: Int + ): String = + io { + VotingRustBackend.generateVanWitnessJson(dbHandle, roundId, bundleIndex, anchorHeight) + ?: error("generateVanWitnessJson returned null") + } + + // ─── Vote commitment ────────────────────────────────────────────────────── + + override suspend fun buildVoteCommitment( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + hotkeySeed: ByteArray, + proposalId: Int, + choice: Int, + numOptions: Int, + witnessJson: String, + vanPosition: Int, + anchorHeight: Int, + networkId: Int, + singleShare: Boolean + ): VoteCommitmentResult = + io { + val json = VotingRustBackend.buildVoteCommitmentJson( + dbHandle, roundId, bundleIndex, hotkeySeed, proposalId, choice, numOptions, + witnessJson, vanPosition, anchorHeight, networkId, singleShare + ) ?: error("buildVoteCommitment returned null") + + val obj = org.json.JSONObject(json) + val encSharesArray = obj.getJSONArray("enc_shares") + val encSharesJson = encSharesArray.toString() + + VoteCommitmentResult( + vanNullifier = hexDec(obj.getString("van_nullifier")), + voteAuthorityNoteNew = hexDec(obj.getString("vote_authority_note_new")), + voteCommitment = hexDec(obj.getString("vote_commitment")), + rVpk = hexDec(obj.getString("r_vpk_bytes")), + alphaV = hexDec(obj.getString("alpha_v")), + anchorHeight = obj.getInt("anchor_height"), + encSharesJson = encSharesJson, + rawBundleJson = json + ) + } + + // ─── Share payloads ─────────────────────────────────────────────────────── + + override suspend fun buildSharePayloadsJson( + encSharesJson: String, + commitmentJson: String, + voteDecision: Int, + numOptions: Int, + vcTreePosition: Long, + singleShareMode: Boolean + ): String = + io { + VotingRustBackend.buildSharePayloadsJson( + encSharesJson, commitmentJson, voteDecision, numOptions, vcTreePosition, singleShareMode + ) ?: error("buildSharePayloads returned null") + } + + // ─── Cast vote ──────────────────────────────────────────────────────────── + + override suspend fun signCastVote( + hotkeySeed: ByteArray, + networkId: Int, + roundId: String, + rVpk: ByteArray, + vanNullifier: ByteArray, + vanNew: ByteArray, + voteCommitment: ByteArray, + proposalId: Int, + anchorHeight: Int, + alphaV: ByteArray + ): ByteArray = + io { + VotingRustBackend.signCastVote( + hotkeySeed, networkId, roundId, rVpk, vanNullifier, vanNew, + voteCommitment, proposalId, anchorHeight, alphaV + ) ?: error("signCastVote returned null") + } + + // ─── Utilities ──────────────────────────────────────────────────────────── + + override suspend fun decomposeWeight(weight: Long): List = + io { + val json = VotingRustBackend.decomposeWeightJson(weight) + val arr = org.json.JSONArray(json) + (0 until arr.length()).map { arr.getLong(it) } + } + + override suspend fun extractOrchardFvkFromUfvk(ufvk: String, networkId: Int): ByteArray = + io { + VotingRustBackend.extractOrchardFvkFromUfvk(ufvk, networkId) + ?: error("extractOrchardFvkFromUfvk returned null") + } + + // ─── Private helpers ───────────────────────────────────────────────────── + + private fun hexDec(hex: String): ByteArray = java.math.BigInteger(hex, 16).toByteArray().let { bytes -> + // BigInteger may prepend a sign byte; strip it if needed + if (hex.length / 2 < bytes.size) bytes.copyOfRange(1, bytes.size) else bytes + } + + private fun parseDelegationSubmission(json: String): DelegationSubmissionResult { + val obj = org.json.JSONObject(json) + val govNullifiers = obj.getJSONArray("gov_nullifiers") + .let { arr -> (0 until arr.length()).map { hexDec(arr.getString(it)) } } + return DelegationSubmissionResult( + proof = hexDec(obj.getString("proof")), + rk = hexDec(obj.getString("rk")), + spendAuthSig = hexDec(obj.getString("spend_auth_sig")), + sighash = hexDec(obj.getString("sighash")), + nfSigned = hexDec(obj.getString("nf_signed")), + cmxNew = hexDec(obj.getString("cmx_new")), + govComm = hexDec(obj.getString("gov_comm")), + govNullifiers = govNullifiers, + alpha = hexDec(obj.getString("alpha")), + voteRoundId = obj.getString("vote_round_id") + ) + } + + private suspend fun io(block: () -> T): T = withContext(Dispatchers.IO) { block() } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt new file mode 100644 index 000000000..6f626c8f3 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt @@ -0,0 +1,336 @@ +package cash.z.ecc.android.sdk.internal.jni + +import cash.z.ecc.android.sdk.internal.model.voting.FfiBundleSetupResult +import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState +import cash.z.ecc.android.sdk.internal.model.voting.FfiVotingHotkey + +/** + * JNI interface for the shielded voting backend (zcash_voting Rust crate). + * + * Rust implementation: backend-lib/src/main/rust/voting.rs + * Crate: https://github.com/valargroup/zcash_voting (branch greg/orchard-0.12) + * iOS reference: valargroup/zcash-swift-wallet-sdk rust/src/voting.rs + * + * Complex return types (GovernancePczt, DelegationProofResult, VoteCommitmentBundle, + * SharePayload, VanWitness, WitnessData) are serialised as JSON strings by the Rust layer. + * Byte arrays in JSON are hex-encoded lowercase strings. + */ +@Suppress("TooManyFunctions") +internal object VotingRustBackend { + + // ─── Database lifecycle ──────────────────────────────────────────────────── + + /** Opens (or creates) the voting SQLite database. Returns opaque handle or 0 on error. */ + @JvmStatic + external fun openVotingDb(dbPath: String): Long + + /** Frees the handle returned by [openVotingDb]. */ + @JvmStatic + external fun closeVotingDb(dbHandle: Long) + + /** Associates a wallet identity string with this database (multi-wallet support). */ + @JvmStatic + external fun setWalletId(dbHandle: Long, walletId: String): Boolean + + // ─── Round management ───────────────────────────────────────────────────── + + @JvmStatic + external fun initRound( + dbHandle: Long, + roundId: String, + snapshotHeight: Long, + eaPK: ByteArray, + ncRoot: ByteArray, + nullifierIMTRoot: ByteArray, + sessionJson: String? + ): Boolean + + /** Returns current phase and metadata for the round, or null on error. */ + @JvmStatic + external fun getRoundState(dbHandle: Long, roundId: String): FfiRoundState? + + /** Returns JSON array of round summaries. */ + @JvmStatic + external fun listRoundsJson(dbHandle: Long): String + + @JvmStatic + external fun clearRound(dbHandle: Long, roundId: String): Boolean + + // ─── Note setup ─────────────────────────────────────────────────────────── + + /** + * Splits [notesJson] (JSON array of NoteInfo) into voting bundles. + * Returns bundle count + eligible weight, or null on error. + */ + @JvmStatic + external fun setupBundles( + dbHandle: Long, + roundId: String, + notesJson: String + ): FfiBundleSetupResult? + + // ─── Hotkey generation ──────────────────────────────────────────────────── + + /** + * Generates a per-round Pallas voting hotkey from [seed] (≥ 32 bytes). + * Returns [FfiVotingHotkey] with secret/public key and "sv1…" address, or null on error. + * Caller must securely erase [seed] after use. + */ + @JvmStatic + external fun generateHotkey( + dbHandle: Long, + roundId: String, + seed: ByteArray + ): FfiVotingHotkey? + + // ─── Governance PCZT ───────────────────────────────────────────────────── + + /** + * Builds a governance PCZT for Keystone signing. + * + * @param notesJson JSON array of NoteInfo for this bundle (from wallet at snapshotHeight) + * @param hotkeyRawSeed 32-byte hotkey seed — used to derive the 43-byte Orchard hotkey address + * @param seedFingerprint 32-byte ZIP-32 seed fingerprint of the wallet + * @param roundName Human-readable round name from the session data + * @param addressIndex ZIP-32 address index (normally 0) + * + * Returns JSON-encoded GovernancePczt, or null on error. + */ + @JvmStatic + external fun buildGovernancePcztJson( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + ufvk: String, + networkId: Int, + accountIndex: Int, + notesJson: String, + hotkeyRawSeed: ByteArray, + seedFingerprint: ByteArray, + roundName: String, + addressIndex: Int + ): String? + + /** Extracts the ZIP-244 sighash from a PCZT blob. Returns 32 bytes or null. */ + @JvmStatic + external fun extractPcztSighash(pcztBytes: ByteArray): ByteArray? + + // ─── Witnesses (required before buildAndProveDelegationJson) ───────────── + + /** + * Caches Merkle witnesses for all notes in a bundle. + * Must be called before [buildAndProveDelegationJson]. + * + * @param witnessesJson JSON array of WitnessData (one per note in the bundle) + */ + @JvmStatic + external fun storeWitnesses( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + witnessesJson: String + ): Boolean + + // ─── Delegation proof (ZKP1) ─────────────────────────────────────────────── + + /** + * Generates the Halo2 delegation proof. Long-running — call on a background coroutine. + * + * @param notesJson JSON array of NoteInfo (same notes used in [storeWitnesses]) + * @param hotkeyRawSeed 32-byte hotkey seed (re-derives Orchard hotkey address internally) + * + * Returns JSON-encoded DelegationProofResult, or null on error. + */ + @JvmStatic + external fun buildAndProveDelegationJson( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + pirServerUrl: String, + networkId: Int, + notesJson: String, + hotkeyRawSeed: ByteArray + ): String? + + // ─── Vote commitment tree ───────────────────────────────────────────────── + + /** + * Syncs the vote commitment tree from the chain node. + * Returns the latest synced block height, or -1 on error. + */ + @JvmStatic + external fun syncVoteTree( + dbHandle: Long, + roundId: String, + nodeUrl: String + ): Long + + /** + * Stores the VAN leaf position after the delegation TX is confirmed on chain. + * Must be called before [generateVanWitnessJson]. + */ + @JvmStatic + external fun storeVanPosition( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + position: Int + ): Boolean + + /** + * Generates a VAN Merkle witness. Must be called after [syncVoteTree] and [storeVanPosition]. + * Returns JSON-encoded VanWitness (auth_path, position, anchor_height), or null on error. + */ + @JvmStatic + external fun generateVanWitnessJson( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + anchorHeight: Int + ): String? + + // ─── Tree state + note witnesses ───────────────────────────────────────── + + /** + * Caches the lightwalletd TreeState protobuf for the snapshot height. + * Must be called before [generateNoteWitnessesJson]. + */ + @JvmStatic + external fun storeTreeState( + dbHandle: Long, + roundId: String, + treeStateBytes: ByteArray + ): Boolean + + /** + * Fetches unspent Orchard notes at [snapshotHeight] from the wallet database. + * [accountUuidBytes] must be exactly 16 bytes (UUID representation). + * Returns JSON array of NoteInfo (hex-encoded fields), or null on error. + */ + @JvmStatic + external fun getWalletNotesJson( + walletDbPath: String, + snapshotHeight: Long, + networkId: Int, + accountUuidBytes: ByteArray + ): String? + + /** + * Generates Orchard Merkle witnesses for notes in a bundle. + * Requires [storeTreeState] to have been called first. + * Internally calls [storeWitnesses] to cache results in the voting DB. + * Returns JSON array of WitnessData, or null on error. + */ + @JvmStatic + external fun generateNoteWitnessesJson( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + walletDbPath: String, + notesJson: String + ): String? + + // ─── Vote commitment (ZKP2) ──────────────────────────────────────────────── + + /** + * Builds the vote commitment proof for one proposal choice. + * + * @param witnessJson JSON-encoded VanWitness returned by [generateVanWitnessJson] + * @param singleShare true for single-share mode (test/dev only) + * + * Returns JSON-encoded VoteCommitmentBundle (wire-safe — no secret share fields). + */ + @JvmStatic + external fun buildVoteCommitmentJson( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + hotkeySeed: ByteArray, + proposalId: Int, + choice: Int, + numOptions: Int, + witnessJson: String, + vanPosition: Int, + anchorHeight: Int, + networkId: Int, + singleShare: Boolean + ): String? + + // ─── Share payloads ──────────────────────────────────────────────────────── + + /** + * Builds share payloads for distribution to tally-server helpers. + * + * @param encSharesJson JSON array of WireEncryptedShare extracted from the bundle + * @param commitmentJson Full JSON-encoded VoteCommitmentBundle from [buildVoteCommitmentJson] + * @param vcTreePosition Position of the VC leaf after cast-vote TX is confirmed + * + * Returns JSON array of SharePayload, or null on error. + */ + @JvmStatic + external fun buildSharePayloadsJson( + encSharesJson: String, + commitmentJson: String, + voteDecision: Int, + numOptions: Int, + vcTreePosition: Long, + singleShareMode: Boolean + ): String? + + // ─── Cast vote signature ─────────────────────────────────────────────────── + + /** Signs the cast-vote message. Returns 64-byte RedPallas signature, or null. */ + @JvmStatic + external fun signCastVote( + hotkeySeed: ByteArray, + networkId: Int, + roundId: String, + rVpk: ByteArray, + vanNullifier: ByteArray, + vanNew: ByteArray, + voteCommitment: ByteArray, + proposalId: Int, + anchorHeight: Int, + alphaV: ByteArray + ): ByteArray? + + // ─── Delegation submission ───────────────────────────────────────────────── + + /** + * Reconstructs the chain-ready delegation TX payload from DB + wallet seed. + * Signs the ZIP-244 sighash with the randomised spending key. + * Returns JSON-encoded DelegationSubmissionData, or null on error. + */ + @JvmStatic + external fun getDelegationSubmissionJson( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + senderSeed: ByteArray, + networkId: Int, + accountIndex: Int + ): String? + + /** + * Reconstructs the delegation TX payload using a Keystone-provided signature. + * Returns JSON-encoded DelegationSubmissionData, or null on error. + */ + @JvmStatic + external fun getDelegationSubmissionWithKeystoneSigJson( + dbHandle: Long, + roundId: String, + bundleIndex: Int, + keystoneSig: ByteArray, + keystoneSighash: ByteArray + ): String? + + // ─── Stateless utilities ────────────────────────────────────────────────── + + /** Decomposes [weight] into power-of-2 share components. Returns JSON array of u64. */ + @JvmStatic + external fun decomposeWeightJson(weight: Long): String + + /** Extracts 96-byte Orchard FVK from a UFVK string. Returns bytes or null. */ + @JvmStatic + external fun extractOrchardFvkFromUfvk(ufvk: String, networkId: Int): ByteArray? +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt new file mode 100644 index 000000000..2ece5ef1a --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt @@ -0,0 +1,141 @@ +package cash.z.ecc.android.sdk.internal.model.voting + +import cash.z.ecc.android.sdk.internal.jni.JNI_HOTKEY_PUBLIC_KEY_BYTES_SIZE +import cash.z.ecc.android.sdk.internal.jni.JNI_HOTKEY_SECRET_KEY_BYTES_SIZE + +/** + * FFI-level data models returned by [VotingRustBackend]. + * + * These mirror the #[repr(C)] structs in voting.rs and the FfiVotingHotkey / + * FfiRoundState / FfiBundleSetupResult types in the iOS VotingRustBackend.swift. + * + * Complex types (GovernancePczt, DelegationProofResult, VoteCommitmentBundle, + * SharePayload, WitnessData) are serialised as JSON strings by the Rust layer and + * decoded in the TypesafeVotingBackend wrapper. + */ + +/** + * 32-byte Orchard spending key derived from the hotkey seed. + * Sensitive: [toString] does NOT log the key bytes. + */ +@ConsistentCopyVisibility +data class HotkeySecretKey internal constructor(val value: ByteArray) { + init { + require(value.size == JNI_HOTKEY_SECRET_KEY_BYTES_SIZE) { + "HotkeySecretKey must be $JNI_HOTKEY_SECRET_KEY_BYTES_SIZE bytes, got ${value.size}" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is HotkeySecretKey) return false + return value.contentEquals(other.value) + } + + override fun hashCode(): Int = value.contentHashCode() + + /** Does NOT log the key bytes to prevent accidental secret exposure. */ + override fun toString(): String = "HotkeySecretKey(size=${value.size})" + + companion object { + fun new(bytes: ByteArray) = HotkeySecretKey(bytes) + } +} + +/** + * 32-byte Pallas public key for the voting hotkey. + */ +@ConsistentCopyVisibility +data class HotkeyPublicKey internal constructor(val value: ByteArray) { + init { + require(value.size == JNI_HOTKEY_PUBLIC_KEY_BYTES_SIZE) { + "HotkeyPublicKey must be $JNI_HOTKEY_PUBLIC_KEY_BYTES_SIZE bytes, got ${value.size}" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is HotkeyPublicKey) return false + return value.contentEquals(other.value) + } + + override fun hashCode(): Int = value.contentHashCode() + + override fun toString(): String = "HotkeyPublicKey(${value.toHexString()})" + + companion object { + fun new(bytes: ByteArray) = HotkeyPublicKey(bytes) + } +} + +private fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) } + +/** + * A per-round voting hotkey returned by the JNI layer. + * + * The primary constructor takes typed [HotkeySecretKey]/[HotkeyPublicKey] values so that + * the generated [data class] `equals`, `hashCode`, `copy`, and `toString` work correctly. + * The secondary `internal constructor(ByteArray, ByteArray, String)` matches the JNI + * signature `([B[BLjava/lang/String;)V` used by voting.rs `make_ffi_voting_hotkey`. + * + * [secretKey] 32-byte Orchard spending key — store securely in VotingStorageDataSource. + * [publicKey] 32-byte public key. + * [address] "sv1…" Bech32 address string. + */ +@ConsistentCopyVisibility +data class FfiVotingHotkey internal constructor( + val secretKey: HotkeySecretKey, + val publicKey: HotkeyPublicKey, + val address: String +) { + /** JNI entry point — voting.rs calls `([B[BLjava/lang/String;)V`. */ + internal constructor(sk: ByteArray, pk: ByteArray, addr: String) : + this(HotkeySecretKey.new(sk), HotkeyPublicKey.new(pk), addr) +} + +/** + * Result of [VotingRustBackend.setupBundles]. + * + * [bundleCount] Number of voting bundles created (one per eligible note group). + * [eligibleWeight] Total voting weight in zatoshi across all bundles. + */ +data class FfiBundleSetupResult( + val bundleCount: Int, + val eligibleWeight: Long +) + +/** + * Current state of a voting round. + * + * [roundId] Hex-encoded round ID. + * [phase] Current protocol phase (0–4, see [FfiRoundPhase]). + * [snapshotHeight] Snapshot block height. + * [hotkeyAddress] "sv1…" address if hotkey generated, null otherwise. + * [delegatedWeight] Zatoshi delegated so far, null if not yet delegated. + * [proofGenerated] Whether the delegation ZK proof has been generated. + */ +data class FfiRoundState( + val roundId: String, + val phase: Int, + val snapshotHeight: Long, + val hotkeyAddress: String?, + val delegatedWeight: Long?, + val proofGenerated: Boolean +) { + val roundPhase: FfiRoundPhase get() = FfiRoundPhase.fromInt(phase) +} + +/** + * Round phase values matching RoundPhase enum in zcash_voting/src/types.rs. + */ +enum class FfiRoundPhase(val value: Int) { + INITIALIZED(0), + HOTKEY_GENERATED(1), + DELEGATION_CONSTRUCTED(2), + DELEGATION_PROVED(3), + VOTE_READY(4); + + companion object { + fun fromInt(value: Int) = entries.firstOrNull { it.value == value } ?: INITIALIZED + } +}