diff --git a/CHANGELOG.md b/CHANGELOG.md index 778bcdbe..2933de14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,18 @@ be considered breaking changes. of a v6 transaction under a new `ironwood` key. Ironwood actions are Orchard-shaped, so the object has the same shape as the existing `orchard` one. Previously the Ironwood bundle was omitted from the decoded output. +- JSON-RPC methods for working with PCZTs (Partially Created Zcash + Transactions), the robust replacements for `createrawtransaction`, + `fundrawtransaction`, and `signrawtransaction`. A transaction is assembled by + threading a PCZT through these roles: + - `pczt_create` — build a PCZT from a payment request (select inputs and + compute change), the replacement for `createrawtransaction` + + `fundrawtransaction`. + - `pczt_combine` — merge the contributions of several parties into one PCZT. + - `pczt_prove` — add the Sapling and/or Orchard zero-knowledge proofs. + - `pczt_sign` — add signatures using the wallet's keys. + - `pczt_extract` — verify the PCZT and extract the final, network-ready + transaction. ### Changed diff --git a/Cargo.lock b/Cargo.lock index 566dcbe9..83672107 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -693,6 +693,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -1043,6 +1044,41 @@ dependencies = [ "petgraph 0.7.1", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deadpool" version = "0.12.3" @@ -2979,6 +3015,34 @@ dependencies = [ "password-hash", ] +[[package]] +name = "pczt" +version = "0.7.0" +source = "git+https://github.com/zcash/librustzcash.git?rev=29c7fb28889a69df15b44fc9465cf4a464f766e9#29c7fb28889a69df15b44fc9465cf4a464f766e9" +dependencies = [ + "blake2b_simd", + "bls12_381", + "document-features", + "ff", + "getset", + "jubjub", + "nonempty", + "orchard 0.15.0-pre.2", + "pasta_curves", + "postcard", + "rand_core 0.6.4", + "redjubjub", + "sapling-crypto", + "secp256k1", + "serde", + "serde_with", + "zcash_note_encryption", + "zcash_primitives 0.29.0-pre.0", + "zcash_protocol 0.10.0-pre.0", + "zcash_script", + "zcash_transparent 0.9.0-pre.0", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3955,6 +4019,33 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_with" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serdect" version = "0.2.0" @@ -4932,6 +5023,7 @@ checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "getrandom 0.4.3", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -5505,6 +5597,7 @@ dependencies = [ "nonempty", "once_cell", "orchard 0.15.0-pre.2", + "pczt", "phf", "proptest", "quote", @@ -5606,13 +5699,16 @@ dependencies = [ "nonempty", "orchard 0.15.0-pre.2", "pasta_curves", + "pczt", "percent-encoding", + "postcard", "prost", "rand_core 0.6.4", "rayon", "sapling-crypto", "secp256k1", "secrecy 0.8.0", + "serde", "shardtree", "subtle", "time", @@ -5664,6 +5760,7 @@ dependencies = [ "schemerz-rusqlite", "secp256k1", "secrecy 0.8.0", + "serde", "shardtree", "static_assertions", "subtle", diff --git a/Cargo.toml b/Cargo.toml index 40eb9bad..d7dc2695 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,6 +115,15 @@ zcash_protocol = "0.10.0-pre.0" # Zcash payment protocols orchard = "0.15.0-pre.2" +pczt = { version = "0.7", features = [ + "orchard", + "sapling", + "transparent", + "prover", + "signer", + "spend-finalizer", + "tx-extractor", +] } sapling = { package = "sapling-crypto", version = "0.7" } transparent = { package = "zcash_transparent", version = "0.9.0-pre.0" } zcash_encoding = "0.4" @@ -177,6 +186,7 @@ zcash_encoding = { git = "https://github.com/zcash/librustzcash.git", rev = "29c # NOTE: zcash_history is deliberately NOT patched here: the root graph does not # use it (only the backend workspaces do, and their patch blocks carry it). zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } +pczt = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } zcash_protocol = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } diff --git a/backends/zaino/Cargo.lock b/backends/zaino/Cargo.lock index 48e3ffb1..1cb66a1f 100644 --- a/backends/zaino/Cargo.lock +++ b/backends/zaino/Cargo.lock @@ -1351,7 +1351,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1497,7 +1497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2646,7 +2646,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2893,7 +2893,7 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3270,7 +3270,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3594,6 +3594,34 @@ dependencies = [ "password-hash", ] +[[package]] +name = "pczt" +version = "0.7.0" +source = "git+https://github.com/zcash/librustzcash.git?rev=29c7fb28889a69df15b44fc9465cf4a464f766e9#29c7fb28889a69df15b44fc9465cf4a464f766e9" +dependencies = [ + "blake2b_simd", + "bls12_381", + "document-features", + "ff", + "getset", + "jubjub", + "nonempty 0.11.0", + "orchard 0.15.0-pre.2", + "pasta_curves", + "postcard", + "rand_core 0.6.4", + "redjubjub", + "sapling-crypto", + "secp256k1", + "serde", + "serde_with", + "zcash_note_encryption", + "zcash_primitives 0.29.0-pre.0", + "zcash_protocol 0.10.0-pre.0", + "zcash_script", + "zcash_transparent 0.9.0-pre.0", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -4020,7 +4048,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4569,7 +4597,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4628,7 +4656,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5308,7 +5336,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6061,6 +6089,7 @@ checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "getrandom 0.4.3", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -6344,7 +6373,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -6415,15 +6444,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -6466,21 +6486,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -6503,12 +6508,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6521,12 +6520,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6539,12 +6532,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6563,12 +6550,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6581,12 +6562,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6599,12 +6574,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6617,12 +6586,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6865,6 +6828,7 @@ dependencies = [ "nix 0.29.0", "nonempty 0.11.0", "orchard 0.15.0-pre.2", + "pczt", "phf", "quote", "rand 0.8.6", @@ -7003,13 +6967,16 @@ dependencies = [ "nonempty 0.11.0", "orchard 0.15.0-pre.2", "pasta_curves", + "pczt", "percent-encoding", + "postcard", "prost", "rand_core 0.6.4", "rayon", "sapling-crypto", "secp256k1", "secrecy 0.8.0", + "serde", "shardtree", "subtle", "time", @@ -7061,6 +7028,7 @@ dependencies = [ "schemerz-rusqlite", "secp256k1", "secrecy 0.8.0", + "serde", "shardtree", "static_assertions", "subtle", diff --git a/backends/zaino/Cargo.toml b/backends/zaino/Cargo.toml index 828d4c32..8ed5a733 100644 --- a/backends/zaino/Cargo.toml +++ b/backends/zaino/Cargo.toml @@ -91,6 +91,7 @@ zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb2 zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } zcash_protocol = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } +pczt = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } # Replicated from librustzcash b43a4237's own [patch.crates-io]: main librustzcash is diff --git a/backends/zaino/supply-chain/config.toml b/backends/zaino/supply-chain/config.toml index 7ab882a6..2591e3e9 100644 --- a/backends/zaino/supply-chain/config.toml +++ b/backends/zaino/supply-chain/config.toml @@ -42,6 +42,9 @@ audit-as-crates-io = true [policy."orchard:0.15.0-pre.2@git:475ef0ff77d45aebff93cb039d639250d82518a3"] audit-as-crates-io = true +[policy.pczt] +audit-as-crates-io = false + [policy.shardtree] audit-as-crates-io = true @@ -468,14 +471,26 @@ criteria = "safe-to-deploy" version = "0.13.4" criteria = "safe-to-deploy" +[[exemptions.darling]] +version = "0.21.3" +criteria = "safe-to-deploy" + [[exemptions.darling_core]] version = "0.13.4" criteria = "safe-to-deploy" +[[exemptions.darling_core]] +version = "0.21.3" +criteria = "safe-to-deploy" + [[exemptions.darling_macro]] version = "0.20.11" criteria = "safe-to-deploy" +[[exemptions.darling_macro]] +version = "0.21.3" +criteria = "safe-to-deploy" + [[exemptions.dashmap]] version = "6.2.1" criteria = "safe-to-deploy" @@ -1584,6 +1599,14 @@ criteria = "safe-to-deploy" version = "3.17.0" criteria = "safe-to-deploy" +[[exemptions.serde_with]] +version = "3.17.0" +criteria = "safe-to-deploy" + +[[exemptions.serde_with_macros]] +version = "3.17.0" +criteria = "safe-to-deploy" + [[exemptions.serde_with_macros]] version = "3.17.0" criteria = "safe-to-deploy" diff --git a/backends/zebra/Cargo.lock b/backends/zebra/Cargo.lock index b665b36b..3f2435e2 100644 --- a/backends/zebra/Cargo.lock +++ b/backends/zebra/Cargo.lock @@ -219,7 +219,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -230,7 +230,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1258,7 +1258,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1404,7 +1404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2542,7 +2542,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2745,7 +2745,7 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3100,7 +3100,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3304,7 +3304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -3410,6 +3410,34 @@ dependencies = [ "password-hash", ] +[[package]] +name = "pczt" +version = "0.7.0" +source = "git+https://github.com/zcash/librustzcash.git?rev=29c7fb28889a69df15b44fc9465cf4a464f766e9#29c7fb28889a69df15b44fc9465cf4a464f766e9" +dependencies = [ + "blake2b_simd", + "bls12_381", + "document-features", + "ff", + "getset", + "jubjub", + "nonempty", + "orchard 0.15.0-pre.2", + "pasta_curves", + "postcard", + "rand_core 0.6.4", + "redjubjub", + "sapling-crypto", + "secp256k1", + "serde", + "serde_with", + "zcash_note_encryption", + "zcash_primitives 0.29.0-pre.0", + "zcash_protocol 0.10.0-pre.0", + "zcash_script", + "zcash_transparent 0.9.0-pre.0", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3809,7 +3837,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -4321,7 +4349,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4867,7 +4895,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5017,7 +5045,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5775,6 +5803,7 @@ checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "getrandom 0.4.3", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -6018,7 +6047,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -6080,15 +6109,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -6125,21 +6145,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -6173,12 +6178,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6191,12 +6190,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6209,12 +6202,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6239,12 +6226,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6257,12 +6238,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6275,12 +6250,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6293,12 +6262,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6441,6 +6404,7 @@ dependencies = [ "nix 0.29.0", "nonempty", "orchard 0.15.0-pre.2", + "pczt", "phf", "quote", "rand 0.8.6", @@ -6583,13 +6547,16 @@ dependencies = [ "nonempty", "orchard 0.15.0-pre.2", "pasta_curves", + "pczt", "percent-encoding", + "postcard", "prost", "rand_core 0.6.4", "rayon", "sapling-crypto", "secp256k1", "secrecy 0.8.0", + "serde", "shardtree", "subtle", "time", @@ -6641,6 +6608,7 @@ dependencies = [ "schemerz-rusqlite", "secp256k1", "secrecy 0.8.0", + "serde", "shardtree", "static_assertions", "subtle", diff --git a/backends/zebra/Cargo.toml b/backends/zebra/Cargo.toml index 50764dc2..8f618686 100644 --- a/backends/zebra/Cargo.toml +++ b/backends/zebra/Cargo.toml @@ -92,6 +92,7 @@ zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = zcash_encoding = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } +pczt = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } zcash_protocol = { git = "https://github.com/zcash/librustzcash.git", rev = "29c7fb28889a69df15b44fc9465cf4a464f766e9" } diff --git a/backends/zebra/supply-chain/config.toml b/backends/zebra/supply-chain/config.toml index 3d0c7cd1..cfc01674 100644 --- a/backends/zebra/supply-chain/config.toml +++ b/backends/zebra/supply-chain/config.toml @@ -42,6 +42,9 @@ audit-as-crates-io = true [policy."orchard:0.15.0-pre.2@git:475ef0ff77d45aebff93cb039d639250d82518a3"] audit-as-crates-io = true +[policy.pczt] +audit-as-crates-io = false + [policy.shardtree] audit-as-crates-io = true @@ -436,14 +439,26 @@ criteria = "safe-to-deploy" version = "0.13.4" criteria = "safe-to-deploy" +[[exemptions.darling]] +version = "0.21.3" +criteria = "safe-to-deploy" + [[exemptions.darling_core]] version = "0.13.4" criteria = "safe-to-deploy" +[[exemptions.darling_core]] +version = "0.21.3" +criteria = "safe-to-deploy" + [[exemptions.darling_macro]] version = "0.20.11" criteria = "safe-to-deploy" +[[exemptions.darling_macro]] +version = "0.21.3" +criteria = "safe-to-deploy" + [[exemptions.deadpool]] version = "0.12.3" criteria = "safe-to-deploy" @@ -1468,6 +1483,14 @@ criteria = "safe-to-deploy" version = "3.17.0" criteria = "safe-to-deploy" +[[exemptions.serde_with]] +version = "3.17.0" +criteria = "safe-to-deploy" + +[[exemptions.serde_with_macros]] +version = "3.17.0" +criteria = "safe-to-deploy" + [[exemptions.serde_with_macros]] version = "3.17.0" criteria = "safe-to-deploy" diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 60fac2a2..fb6ba74b 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -42,6 +42,9 @@ audit-as-crates-io = true [policy."orchard:0.15.0-pre.2@git:475ef0ff77d45aebff93cb039d639250d82518a3"] audit-as-crates-io = true +[policy.pczt] +audit-as-crates-io = false + [policy.shardtree] audit-as-crates-io = true @@ -399,6 +402,18 @@ criteria = "safe-to-deploy" version = "0.8.1" criteria = "safe-to-deploy" +[[exemptions.darling]] +version = "0.21.3" +criteria = "safe-to-deploy" + +[[exemptions.darling_core]] +version = "0.21.3" +criteria = "safe-to-deploy" + +[[exemptions.darling_macro]] +version = "0.21.3" +criteria = "safe-to-deploy" + [[exemptions.deadpool]] version = "0.12.3" criteria = "safe-to-deploy" @@ -1235,6 +1250,14 @@ criteria = "safe-to-deploy" version = "1.1.1" criteria = "safe-to-deploy" +[[exemptions.serde_with]] +version = "3.17.0" +criteria = "safe-to-deploy" + +[[exemptions.serde_with_macros]] +version = "3.17.0" +criteria = "safe-to-deploy" + [[exemptions.serdect]] version = "0.2.0" criteria = "safe-to-deploy" diff --git a/zallet-core/Cargo.toml b/zallet-core/Cargo.toml index 310e5447..eefa62c2 100644 --- a/zallet-core/Cargo.toml +++ b/zallet-core/Cargo.toml @@ -91,6 +91,7 @@ zcash_protocol = { workspace = true, features = ["local-consensus"] } # Zcash payment protocols orchard.workspace = true +pczt.workspace = true sapling.workspace = true secp256k1.workspace = true transparent.workspace = true @@ -115,11 +116,13 @@ uuid.workspace = true zcash_client_backend = { workspace = true, features = [ "lightwalletd-tonic-tls-webpki-roots", "orchard", + "pczt", "sync-decryptor", "transparent-inputs", ] } zcash_client_sqlite = { workspace = true, features = [ "orchard", + "serde", "transparent-inputs", ] } zcash_note_encryption.workspace = true diff --git a/zallet-core/src/components/json_rpc/methods.rs b/zallet-core/src/components/json_rpc/methods.rs index 3288122e..42bb3646 100644 --- a/zallet-core/src/components/json_rpc/methods.rs +++ b/zallet-core/src/components/json_rpc/methods.rs @@ -54,6 +54,16 @@ mod list_unspent; mod lock_wallet; #[cfg(zallet_build = "wallet")] mod openrpc; +// PCZT methods. The stateless roles (combine, prove, extract) and their shared +// helpers are available in every build; create and sign require wallet state. +mod pczt_combine; +mod pczt_common; +#[cfg(zallet_build = "wallet")] +mod pczt_create; +mod pczt_extract; +mod pczt_prove; +#[cfg(zallet_build = "wallet")] +mod pczt_sign; #[cfg(zallet_build = "wallet")] mod recover_accounts; mod stop; @@ -274,6 +284,37 @@ pub(crate) trait Rpc { /// - `hexstring` (string, required): The hex-encoded script. #[method(name = "decodescript")] async fn decode_script(&self, hexstring: &str) -> decode_script::Response; + + /// Combines multiple PCZTs (for the same transaction) into one. + /// + /// This applies the Combiner role, merging the per-party contributions + /// (proofs and signatures produced in parallel) into a single PCZT. + /// + /// # Arguments + /// - `pczts` (array, required) The base64-encoded PCZTs to combine. + #[method(name = "pczt_combine")] + async fn pczt_combine(&self, pczts: Vec) -> pczt_combine::Response; + + /// Adds the zero-knowledge proofs required by a PCZT. + /// + /// Creates the Sapling and/or Orchard proofs for the PCZT's shielded + /// components. This must be done before the transaction can be extracted. + /// + /// # Arguments + /// - `pczt` (string, required) The base64-encoded PCZT to add proofs to. + #[method(name = "pczt_prove")] + async fn pczt_prove(&self, pczt: &str) -> pczt_prove::Response; + + /// Extracts the final, network-ready transaction from a completed PCZT. + /// + /// Finalizes the transparent spends and verifies all proofs and signatures + /// before returning the hex-encoded transaction. Fails if the PCZT is not + /// fully proven and signed. + /// + /// # Arguments + /// - `pczt` (string, required) The base64-encoded PCZT to extract from. + #[method(name = "pczt_extract")] + async fn pczt_extract(&self, pczt: &str) -> pczt_extract::Response; } /// The wallet-specific JSON-RPC interface, containing the methods only provided in the @@ -703,6 +744,40 @@ pub(crate) trait WalletRpc { memo: Option, privacy_policy: Option, ) -> z_shieldcoinbase::Response; + + /// Creates a PCZT from a transaction proposal. + /// + /// Selects inputs and computes change for the given recipients, producing a + /// complete (but unproven and unsigned) PCZT. This is the PCZT-based + /// replacement for `createrawtransaction` and `fundrawtransaction`. + /// + /// # Arguments + /// - `from_address` (string, required) The address to send funds from. + /// - `amounts` (array, required) An array of recipient amounts with fields: + /// - `address` (string, required) Recipient address. + /// - `amount` (numeric, required) Amount in ZEC. + /// - `memo` (string, optional) Optional memo for shielded recipients. + /// - `minconf` (numeric, optional) Minimum confirmations for inputs. + /// - `privacy_policy` (string, optional) Privacy policy for the transaction. + #[method(name = "pczt_create")] + async fn pczt_create( + &self, + from_address: String, + amounts: Vec, + minconf: Option, + privacy_policy: Option, + ) -> pczt_create::Response; + + /// Signs a PCZT with the wallet's keys. + /// + /// Signs every input the wallet holds keys for. Inputs belonging to other + /// keys are left unsigned and reported, unless `strict` is set. + /// + /// # Arguments + /// - `pczt` (string, required) The base64-encoded PCZT to sign. + /// - `strict` (bool, optional) If true, fail if any inputs cannot be signed. + #[method(name = "pczt_sign")] + async fn pczt_sign(&self, pczt: &str, strict: Option) -> pczt_sign::Response; } pub(crate) struct RpcImpl { @@ -906,6 +981,18 @@ impl RpcServer for RpcImpl { async fn decode_script(&self, hexstring: &str) -> decode_script::Response { decode_script::call(self.wallet().await?.params(), hexstring) } + + async fn pczt_combine(&self, pczts: Vec) -> pczt_combine::Response { + pczt_combine::call(pczts) + } + + async fn pczt_prove(&self, pczt: &str) -> pczt_prove::Response { + pczt_prove::call(pczt).await + } + + async fn pczt_extract(&self, pczt: &str) -> pczt_extract::Response { + pczt_extract::call(pczt).await + } } #[cfg(zallet_build = "wallet")] @@ -1116,4 +1203,25 @@ impl WalletRpcServer for WalletRpcImpl { let opid = self.start_async((context, fut)).await; Ok(z_shieldcoinbase::ShieldCoinbaseResult::new(preflight, opid)) } + + async fn pczt_create( + &self, + from_address: String, + amounts: Vec, + minconf: Option, + privacy_policy: Option, + ) -> pczt_create::Response { + pczt_create::call( + self.wallet().await?, + from_address, + amounts, + minconf, + privacy_policy, + ) + .await + } + + async fn pczt_sign(&self, pczt: &str, strict: Option) -> pczt_sign::Response { + pczt_sign::call(self.wallet().await?, self.keystore.clone(), pczt, strict).await + } } diff --git a/zallet-core/src/components/json_rpc/methods/pczt_combine.rs b/zallet-core/src/components/json_rpc/methods/pczt_combine.rs new file mode 100644 index 00000000..3c6a8425 --- /dev/null +++ b/zallet-core/src/components/json_rpc/methods/pczt_combine.rs @@ -0,0 +1,75 @@ +//! PCZT combine method — merge multiple PCZTs. + +use documented::Documented; +use jsonrpsee::core::RpcResult; +use pczt::roles::combiner::Combiner; +use schemars::JsonSchema; +use serde::Serialize; + +use super::pczt_common::{MAX_PCZTS_TO_COMBINE, decode_pczt_base64, encode_pczt_base64}; +use crate::components::json_rpc::server::LegacyCode; + +pub(crate) type Response = RpcResult; + +/// Result containing the combined PCZT. +#[derive(Clone, Debug, Serialize, Documented, JsonSchema)] +pub(crate) struct CombineResult { + /// The base64-encoded combined PCZT. + pub pczt: String, +} + +pub(crate) type ResultType = CombineResult; + +pub(super) const PARAM_PCZTS_DESC: &str = "An array of base64-encoded PCZTs to combine."; +pub(super) const PARAM_PCZTS_REQUIRED: bool = true; + +/// Combines multiple PCZTs into a single PCZT. +pub(crate) fn call(pczts_base64: Vec) -> Response { + if pczts_base64.is_empty() { + return Err(LegacyCode::InvalidParameter.with_static("At least one PCZT is required")); + } + + if pczts_base64.len() > MAX_PCZTS_TO_COMBINE { + return Err(LegacyCode::InvalidParameter.with_message(format!( + "Too many PCZTs to combine: {} exceeds maximum of {MAX_PCZTS_TO_COMBINE}", + pczts_base64.len(), + ))); + } + + let pczts = pczts_base64 + .iter() + .enumerate() + .map(|(i, pczt_base64)| { + decode_pczt_base64(pczt_base64).map_err(|e| { + LegacyCode::Deserialization.with_message(format!("PCZT {i}: {}", e.message())) + }) + }) + .collect::, _>>()?; + + let combined = Combiner::new(pczts) + .combine() + .map_err(|_| LegacyCode::Verify.with_static("Failed to combine PCZTs"))?; + + Ok(CombineResult { + pczt: encode_pczt_base64(combined)?, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_empty_input() { + let err = call(vec![]).unwrap_err(); + assert!(err.message().contains("At least one PCZT")); + } + + #[test] + fn rejects_too_many() { + // The cap is enforced before any decoding, so the contents are irrelevant. + let too_many = vec!["AAAA".to_string(); MAX_PCZTS_TO_COMBINE + 1]; + let err = call(too_many).unwrap_err(); + assert!(err.message().contains("Too many PCZTs")); + } +} diff --git a/zallet-core/src/components/json_rpc/methods/pczt_common.rs b/zallet-core/src/components/json_rpc/methods/pczt_common.rs new file mode 100644 index 00000000..b8464668 --- /dev/null +++ b/zallet-core/src/components/json_rpc/methods/pczt_common.rs @@ -0,0 +1,112 @@ +//! Shared helpers for the PCZT RPC methods. + +use base64ct::{Base64, Encoding}; +use jsonrpsee::types::ErrorObjectOwned; +use pczt::Pczt; +use transparent::keys::TransparentKeyScope; + +use crate::components::json_rpc::server::LegacyCode; + +/// Maximum size, in bytes, accepted for a base64-encoded PCZT. +/// +/// PCZTs grow with the number of inputs and outputs (and their proofs), but a +/// 10 MiB ceiling comfortably exceeds any realistic transaction while bounding +/// the work an unauthenticated decode can be made to do. +pub(super) const MAX_PCZT_BASE64_LEN: usize = 10 * 1024 * 1024; + +/// Maximum number of PCZTs accepted by `pczt_combine` in a single call. +pub(super) const MAX_PCZTS_TO_COMBINE: usize = 20; + +// Proprietary-field keys carrying Zallet's signing hints inside a PCZT. +// +// `pczt_create` writes these and `pczt_sign` reads them back — a private +// contract between the two methods (pczt 0.7 exposes no getter for the PCZT's +// native ZIP 32 / BIP 32 derivation metadata, so we stash our own copy). +// Defining them in one place keeps the writer and reader from drifting; the +// `v1` prefix leaves room for the format to evolve. + +/// Global: the wallet seed fingerprint (32 bytes). +pub(super) const PROP_SEED_FINGERPRINT: &str = "zallet.v1.seed_fingerprint"; + +/// Global: the ZIP 32 account index (`u32`, little-endian). +pub(super) const PROP_ACCOUNT_INDEX: &str = "zallet.v1.account_index"; + +/// Per transparent input: the key scope (`u32`, little-endian; see [`encode_key_scope`]). +pub(super) const PROP_SCOPE: &str = "zallet.v1.scope"; + +/// Per transparent input: the non-hardened address index (`u32`, little-endian). +pub(super) const PROP_ADDRESS_INDEX: &str = "zallet.v1.address_index"; + +/// Encodes a transparent key scope as the `u32` stored in the [`PROP_SCOPE`] hint. +/// +/// Inverse of [`decode_key_scope`]. Any scope that is neither external nor +/// internal (e.g. ephemeral) maps to `2`. +pub(super) fn encode_key_scope(scope: TransparentKeyScope) -> u32 { + if scope == TransparentKeyScope::EXTERNAL { + 0 + } else if scope == TransparentKeyScope::INTERNAL { + 1 + } else { + 2 + } +} + +/// Decodes a [`PROP_SCOPE`] `u32` back into a key scope, or `None` if the value +/// is not one this Zallet version writes. +pub(super) fn decode_key_scope(value: u32) -> Option { + match value { + 0 => Some(TransparentKeyScope::EXTERNAL), + 1 => Some(TransparentKeyScope::INTERNAL), + 2 => Some(TransparentKeyScope::EPHEMERAL), + _ => None, + } +} + +/// Decodes a base64-encoded PCZT, rejecting oversized inputs before allocating. +pub(super) fn decode_pczt_base64(s: &str) -> Result { + if s.len() > MAX_PCZT_BASE64_LEN { + return Err(LegacyCode::InvalidParameter.with_static("PCZT exceeds maximum size limit")); + } + let pczt_bytes = Base64::decode_vec(s).map_err(|e| { + LegacyCode::Deserialization.with_message(format!("Invalid base64 encoding: {e}")) + })?; + // The parse error describes the malformed bytes; we surface a generic + // message rather than its internals. + Pczt::parse(&pczt_bytes).map_err(|_| LegacyCode::Deserialization.with_static("Invalid PCZT")) +} + +/// Serializes a PCZT and base64-encodes it for a JSON-RPC response. +/// +/// `Pczt::serialize` consumes the PCZT and can fail if it holds values that +/// exceed the wire format's bounds; that would be an internal inconsistency +/// rather than bad user input, so we surface it as a generic error. +pub(super) fn encode_pczt_base64(pczt: Pczt) -> Result { + let bytes = pczt + .serialize() + .map_err(|_| LegacyCode::Misc.with_static("Failed to serialize PCZT"))?; + Ok(Base64::encode_string(&bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_oversized_input() { + let oversized = "A".repeat(MAX_PCZT_BASE64_LEN + 1); + let err = decode_pczt_base64(&oversized).unwrap_err(); + assert!(err.message().contains("maximum size limit")); + } + + #[test] + fn rejects_invalid_base64() { + let err = decode_pczt_base64("not valid base64 !!!").unwrap_err(); + assert!(err.message().contains("base64")); + } + + #[test] + fn rejects_valid_base64_that_is_not_a_pczt() { + // Valid base64, but not the PCZT magic/format. + assert!(decode_pczt_base64("AAAAAAAA").is_err()); + } +} diff --git a/zallet-core/src/components/json_rpc/methods/pczt_create.rs b/zallet-core/src/components/json_rpc/methods/pczt_create.rs new file mode 100644 index 00000000..77fc9ed5 --- /dev/null +++ b/zallet-core/src/components/json_rpc/methods/pczt_create.rs @@ -0,0 +1,212 @@ +//! PCZT create method — create a PCZT from a transaction proposal. +//! +//! This is the functional replacement for `createrawtransaction` + +//! `fundrawtransaction`: it selects inputs and computes change for a set of +//! recipients, producing a complete (but unproven and unsigned) PCZT. + +use std::convert::Infallible; +use std::num::NonZeroU32; + +use abscissa_core::Application; +use documented::Documented; +use jsonrpsee::core::RpcResult; +use pczt::roles::updater::Updater; +use schemars::JsonSchema; +use serde::Serialize; +use zcash_client_backend::{ + data_api::{ + Account, WalletRead, + wallet::{ConfirmationsPolicy, create_pczt_from_proposal}, + }, + wallet::OvkPolicy, +}; +use zcash_keys::address::Address; + +use super::pczt_common::{ + PROP_ACCOUNT_INDEX, PROP_ADDRESS_INDEX, PROP_SCOPE, PROP_SEED_FINGERPRINT, encode_key_scope, + encode_pczt_base64, +}; +use crate::{ + components::{ + database::DbHandle, + json_rpc::{ + payments::{ + AmountParameter, PrivacyPolicy, build_request, get_account_for_address, + propose_and_check, + }, + server::LegacyCode, + }, + }, + prelude::*, +}; + +/// Maximum number of recipients accepted in a single `pczt_create` call. +/// +/// A funded transaction is ultimately bounded by the consensus size limit and +/// the configured Orchard action limit, but we reject obviously abusive inputs +/// before doing any proposal work. +const MAX_RECIPIENTS: usize = 1000; + +pub(crate) type Response = RpcResult; + +/// Result of creating a PCZT. +#[derive(Clone, Debug, Serialize, Documented, JsonSchema)] +pub(crate) struct CreateResult { + /// The base64-encoded PCZT. + pub pczt: String, +} + +pub(crate) type ResultType = CreateResult; + +pub(super) const PARAM_FROM_ADDRESS_DESC: &str = "The address to send funds from."; +pub(super) const PARAM_AMOUNTS_DESC: &str = "An array of recipient amounts."; +pub(super) const PARAM_AMOUNTS_REQUIRED: bool = true; +pub(super) const PARAM_MINCONF_DESC: &str = "Minimum confirmations for inputs."; +pub(super) const PARAM_PRIVACY_POLICY_DESC: &str = "Privacy policy for the transaction."; + +/// Creates a PCZT from a transaction proposal. +pub(crate) async fn call( + mut wallet: DbHandle, + from_address: String, + amounts: Vec, + minconf: Option, + privacy_policy: Option, +) -> Response { + if amounts.len() > MAX_RECIPIENTS { + return Err(LegacyCode::InvalidParameter.with_message(format!( + "Too many recipients: {} exceeds maximum of {MAX_RECIPIENTS}", + amounts.len(), + ))); + } + + let request = build_request(&amounts)?; + + // Resolve `from_address` to an account. + let account = { + let address = Address::decode(wallet.params(), &from_address).ok_or_else(|| { + LegacyCode::InvalidAddressOrKey + .with_static("Invalid from address: should be a taddr, zaddr, or UA.") + })?; + + get_account_for_address(wallet.as_ref(), &address) + }?; + + let privacy_policy = match privacy_policy.as_deref() { + Some("LegacyCompat") => Err(LegacyCode::InvalidParameter + .with_static("LegacyCompat privacy policy is unsupported in Zallet")), + Some(s) => PrivacyPolicy::from_str(s).ok_or_else(|| { + LegacyCode::InvalidParameter.with_message(format!("Unknown privacy policy {s}")) + }), + None => Ok(PrivacyPolicy::FullPrivacy), + }?; + + let confirmations_policy = match minconf { + Some(minconf) => NonZeroU32::new(minconf).map_or( + ConfirmationsPolicy::new_symmetrical(NonZeroU32::MIN, true), + |c| ConfirmationsPolicy::new_symmetrical(c, false), + ), + None => APP.config().builder.confirmations_policy().map_err(|_| { + LegacyCode::Wallet.with_message( + "Configuration error: minimum confirmations for spending trusted TXOs cannot exceed that for untrusted TXOs.") + })?, + }; + + let params = *wallet.params(); + let proposal = propose_and_check( + wallet.as_mut(), + ¶ms, + account.id(), + request, + privacy_policy, + confirmations_policy, + )?; + + // Derivation info used to populate the zallet signing hints below. + let derivation = account.source().key_derivation().ok_or_else(|| { + LegacyCode::InvalidAddressOrKey + .with_static("Invalid from address, no payment source found for address.") + })?; + + // Build the PCZT from the proposal. This selects inputs, computes change, + // runs IO finalization, and records the native ZIP 32 / BIP 32 derivation + // metadata, but does not create proofs or signatures. + let pczt = create_pczt_from_proposal::<_, _, Infallible, _, Infallible, _>( + wallet.as_mut(), + ¶ms, + account.id(), + OvkPolicy::Sender, + &proposal, + // Do not override the builder-derived expiry height. + None, + // Our proposal uses the default (padded) Orchard change strategy, so the + // bundle type must be `DEFAULT` to match it. + orchard::builder::BundleType::DEFAULT, + ) + .map_err(|e| LegacyCode::Wallet.with_message(format!("Failed to create PCZT: {e}")))?; + + // Collect the per-input transparent derivation info from the proposal, in + // the same order as the PCZT's transparent inputs. + let mut input_metadata = Vec::new(); + for step in proposal.steps() { + for transparent_input in step.transparent_inputs() { + let address = transparent_input.recipient_address(); + let meta = wallet + .get_transparent_address_metadata(account.id(), address) + .ok() + .flatten(); + input_metadata.push(meta); + } + } + + if input_metadata.len() != pczt.transparent().inputs().len() { + return Err( + LegacyCode::Misc.with_static("Internal error: transparent input count mismatch") + ); + } + + // Record signing hints as proprietary fields. The PCZT format does carry + // native ZIP 32 / BIP 32 derivation metadata (populated above), but pczt + // 0.7 exposes no public getter for it, so an offline `pczt_sign` cannot read + // it back. These `zallet.v1.*` fields are a stand-in for that native path + // until the upstream accessors land. + let pczt = Updater::new(pczt) + .update_global_with(|mut global| { + global.set_proprietary( + PROP_SEED_FINGERPRINT.to_string(), + derivation.seed_fingerprint().to_bytes().to_vec(), + ); + global.set_proprietary( + PROP_ACCOUNT_INDEX.to_string(), + u32::from(derivation.account_index()).to_le_bytes().to_vec(), + ); + }) + // A no-op when there are no transparent inputs. + .update_transparent_with(|mut bundle| { + for (index, meta) in input_metadata.iter().enumerate() { + if let Some(meta) = meta { + // Only derived addresses carry a scope and index. + if let (Some(scope), Some(address_index)) = (meta.scope(), meta.address_index()) + { + bundle.update_input_with(index, |mut input| { + input.set_proprietary( + PROP_SCOPE.to_string(), + encode_key_scope(scope).to_le_bytes().to_vec(), + ); + input.set_proprietary( + PROP_ADDRESS_INDEX.to_string(), + address_index.index().to_le_bytes().to_vec(), + ); + Ok(()) + })?; + } + } + } + Ok(()) + }) + .map_err(|_| LegacyCode::Wallet.with_static("Failed to record transparent signing hints"))? + .finish(); + + Ok(CreateResult { + pczt: encode_pczt_base64(pczt)?, + }) +} diff --git a/zallet-core/src/components/json_rpc/methods/pczt_extract.rs b/zallet-core/src/components/json_rpc/methods/pczt_extract.rs new file mode 100644 index 00000000..8d7b822d --- /dev/null +++ b/zallet-core/src/components/json_rpc/methods/pczt_extract.rs @@ -0,0 +1,73 @@ +//! PCZT extract method — extract the final transaction from a completed PCZT. +//! +//! Extraction finalizes the transparent spends and then verifies every proof +//! and signature before producing the transaction bytes. A PCZT that is missing +//! proofs (see [`super::pczt_prove`]) or signatures (see [`super::pczt_sign`]) +//! will be rejected here rather than producing an invalid transaction. + +use documented::Documented; +use jsonrpsee::core::RpcResult; +use jsonrpsee::types::ErrorObjectOwned; +use pczt::roles::spend_finalizer::SpendFinalizer; +use pczt::roles::tx_extractor::TransactionExtractor; +use schemars::JsonSchema; +use serde::Serialize; +use zcash_proofs::prover::LocalTxProver; + +use super::pczt_common::decode_pczt_base64; +use crate::components::json_rpc::server::LegacyCode; + +pub(crate) type Response = RpcResult; + +/// Result containing the extracted transaction. +#[derive(Clone, Debug, Serialize, Documented, JsonSchema)] +pub(crate) struct ExtractResult { + /// The hex-encoded raw transaction. + pub hex: String, +} + +pub(crate) type ResultType = ExtractResult; + +pub(super) const PARAM_PCZT_DESC: &str = + "The base64-encoded PCZT to extract a final transaction from."; + +/// Extracts a final, network-ready transaction from a completed PCZT. +/// +/// The PCZT must already have all required proofs and signatures in place; +/// extraction verifies them and fails otherwise. +pub(crate) async fn call(pczt_base64: &str) -> Response { + let pczt = decode_pczt_base64(pczt_base64)?; + + // Spend finalization and proof verification are CPU-bound (and loading the + // Sapling verifying keys is expensive), so run them off the async runtime. + let tx_bytes: Vec = + crate::spawn_blocking!("pczt_extract", move || -> Result<_, ErrorObjectOwned> { + // Fold partial transparent signatures into their `script_sig`s. This + // is a no-op when there are no transparent inputs. + let pczt = SpendFinalizer::new(pczt) + .finalize_spends() + .map_err(|_| LegacyCode::Verify.with_static("Failed to finalize PCZT spends"))?; + + // Supplying the Sapling verifying keys is required to extract a PCZT + // that has a Sapling bundle. The Orchard verifying key is generated + // on the fly by the extractor when one is not provided. + let (spend_vk, output_vk) = LocalTxProver::bundled().verifying_keys(); + let tx = TransactionExtractor::new(pczt) + .with_sapling(&spend_vk, &output_vk) + .extract() + .map_err(|_| LegacyCode::Verify.with_static("Failed to extract transaction"))?; + + let mut tx_bytes = Vec::new(); + tx.write(&mut tx_bytes).map_err(|e| { + LegacyCode::Deserialization + .with_message(format!("Failed to serialize transaction: {e}")) + })?; + Ok(tx_bytes) + }) + .await + .map_err(|_| LegacyCode::Misc.with_static("Extraction task failed"))??; + + Ok(ExtractResult { + hex: hex::encode(tx_bytes), + }) +} diff --git a/zallet-core/src/components/json_rpc/methods/pczt_prove.rs b/zallet-core/src/components/json_rpc/methods/pczt_prove.rs new file mode 100644 index 00000000..bf5828d5 --- /dev/null +++ b/zallet-core/src/components/json_rpc/methods/pczt_prove.rs @@ -0,0 +1,91 @@ +//! PCZT prove method — create the zero-knowledge proofs for a PCZT. +//! +//! Proving is a prerequisite for extracting a transaction from any PCZT that +//! has shielded components: [`super::pczt_extract`] verifies proofs and will +//! reject a PCZT whose Sapling or Orchard proofs are missing. + +use std::sync::OnceLock; + +use documented::Documented; +use jsonrpsee::core::RpcResult; +use jsonrpsee::types::ErrorObjectOwned; +use pczt::Pczt; +use pczt::roles::prover::Prover; +use schemars::JsonSchema; +use serde::Serialize; +use zcash_proofs::prover::LocalTxProver; + +use super::pczt_common::{decode_pczt_base64, encode_pczt_base64}; +use crate::components::json_rpc::server::LegacyCode; + +pub(crate) type Response = RpcResult; + +/// Result of proving a PCZT. +#[derive(Clone, Debug, Serialize, Documented, JsonSchema)] +pub(crate) struct ProveResult { + /// The base64-encoded PCZT with proofs added. + pub pczt: String, + /// Whether Sapling proofs were created. + pub sapling_proven: bool, + /// Whether the Orchard proof was created. + pub orchard_proven: bool, +} + +pub(crate) type ResultType = ProveResult; + +pub(super) const PARAM_PCZT_DESC: &str = "The base64-encoded PCZT to add proofs to."; + +/// Returns the Orchard proving key, building it once and caching it. +/// +/// `ProvingKey::build` takes several seconds, so we avoid rebuilding it on +/// every call. The key is held for the lifetime of the process. +/// +/// We build the `FixedPostNu6_2` circuit, which is the one used for all current +/// (non-Ironwood) network proving; `create_orchard_proof` proves the regular +/// Orchard bundle against it. +fn orchard_proving_key() -> &'static orchard::circuit::ProvingKey { + static ORCHARD_PK: OnceLock = OnceLock::new(); + ORCHARD_PK.get_or_init(|| { + orchard::circuit::ProvingKey::build(orchard::circuit::OrchardCircuitVersion::FixedPostNu6_2) + }) +} + +/// Creates the Sapling and/or Orchard proofs required by a PCZT. +pub(crate) async fn call(pczt_base64: &str) -> Response { + let prover = Prover::new(decode_pczt_base64(pczt_base64)?); + + let need_sapling = prover.requires_sapling_proofs(); + let need_orchard = prover.requires_orchard_proof(); + + // Proving is CPU-bound (and loading the Sapling parameters is expensive), so + // run it off the async runtime. + let (pczt, sapling_proven, orchard_proven): (Pczt, bool, bool) = + crate::spawn_blocking!("pczt_prove", move || -> Result<_, ErrorObjectOwned> { + let mut prover = prover; + + if need_sapling { + let local = LocalTxProver::bundled(); + prover = prover.create_sapling_proofs(&local, &local).map_err(|_| { + LegacyCode::Verify.with_static("Failed to create Sapling proofs") + })?; + } + + if need_orchard { + prover = prover + .create_orchard_proof(orchard_proving_key()) + .map_err(|_| { + LegacyCode::Verify.with_static("Failed to create Orchard proof") + })?; + } + + Ok((prover.finish(), need_sapling, need_orchard)) + }) + .await + .map_err(|_| LegacyCode::Misc.with_static("Proving task failed"))??; + + Ok(ProveResult { + pczt: encode_pczt_base64(pczt)?, + sapling_proven, + orchard_proven, + }) +} diff --git a/zallet-core/src/components/json_rpc/methods/pczt_sign.rs b/zallet-core/src/components/json_rpc/methods/pczt_sign.rs new file mode 100644 index 00000000..34ee5db0 --- /dev/null +++ b/zallet-core/src/components/json_rpc/methods/pczt_sign.rs @@ -0,0 +1,224 @@ +//! PCZT sign method — sign a PCZT with the wallet's keys. + +use documented::Documented; +use jsonrpsee::core::RpcResult; +use pczt::roles::signer::Signer; +use schemars::JsonSchema; +use secrecy::ExposeSecret; +use serde::Serialize; +use transparent::keys::{NonHardenedChildIndex, TransparentKeyScope}; +use zcash_keys::keys::UnifiedSpendingKey; +use zip32::{AccountId, fingerprint::SeedFingerprint}; + +use super::pczt_common::{ + PROP_ACCOUNT_INDEX, PROP_ADDRESS_INDEX, PROP_SCOPE, PROP_SEED_FINGERPRINT, decode_key_scope, + decode_pczt_base64, encode_pczt_base64, +}; +use crate::components::{database::DbHandle, json_rpc::server::LegacyCode, keystore::KeyStore}; + +pub(crate) type Response = RpcResult; + +/// Result of signing a PCZT. +#[derive(Clone, Debug, Serialize, Documented, JsonSchema)] +pub(crate) struct SignResult { + /// The base64-encoded signed PCZT. + pub pczt: String, + /// Number of transparent inputs signed. + pub transparent_signed: usize, + /// Number of Sapling spends signed. + pub sapling_signed: usize, + /// Number of Orchard actions signed. + pub orchard_signed: usize, + /// Indices of transparent inputs that could not be signed. + pub unsigned_transparent: Vec, + /// Indices of Sapling spends that could not be signed. + pub unsigned_sapling: Vec, + /// Indices of Orchard actions that could not be signed. + pub unsigned_orchard: Vec, +} + +pub(crate) type ResultType = SignResult; + +pub(super) const PARAM_PCZT_DESC: &str = "The base64-encoded PCZT to sign."; +pub(super) const PARAM_STRICT_DESC: &str = "If true, fail if any inputs cannot be signed."; + +/// Signs a PCZT with the wallet's keys. +/// +/// Signs every input that the wallet holds keys for, using the account +/// identified by the `zallet.v1.*` signing hints that `pczt_create` records. +/// Inputs that do not belong to this wallet are left unsigned and reported. +pub(crate) async fn call( + wallet: DbHandle, + keystore: KeyStore, + pczt_base64: &str, + strict: Option, +) -> Response { + let pczt = decode_pczt_base64(pczt_base64)?; + + // Read the account signing hints from the global proprietary fields. + let seed_fp_bytes = pczt + .global() + .proprietary() + .get(PROP_SEED_FINGERPRINT) + .ok_or_else(|| { + LegacyCode::InvalidParameter + .with_message(format!("Missing signing hint: {PROP_SEED_FINGERPRINT}")) + })?; + + let seed_fp = + SeedFingerprint::from_bytes(seed_fp_bytes.as_slice().try_into().map_err(|_| { + LegacyCode::InvalidParameter.with_static("Invalid seed fingerprint: expected 32 bytes") + })?); + + let account_idx_bytes = pczt + .global() + .proprietary() + .get(PROP_ACCOUNT_INDEX) + .ok_or_else(|| { + LegacyCode::InvalidParameter + .with_message(format!("Missing signing hint: {PROP_ACCOUNT_INDEX}")) + })?; + + let account_idx_u32 = + u32::from_le_bytes(account_idx_bytes.as_slice().try_into().map_err(|_| { + LegacyCode::InvalidParameter.with_static("Invalid account index: expected 4 bytes") + })?); + + let account_idx = AccountId::try_from(account_idx_u32).map_err(|_| { + LegacyCode::InvalidParameter.with_message(format!( + "Invalid account index: {account_idx_u32} is out of range" + )) + })?; + + // Read the per-input transparent derivation hints before the Signer + // consumes the PCZT. + let transparent_derivation_info: Vec> = + pczt.transparent() + .inputs() + .iter() + .enumerate() + .map(|(i, input)| { + let scope_bytes = input.proprietary().get(PROP_SCOPE)?; + let addr_idx_bytes = input.proprietary().get(PROP_ADDRESS_INDEX)?; + + let scope_u32 = match scope_bytes.as_slice().try_into() { + Ok(bytes) => u32::from_le_bytes(bytes), + Err(_) => { + tracing::warn!("Malformed {PROP_SCOPE} for transparent input {i}"); + return None; + } + }; + let addr_idx_u32 = match addr_idx_bytes.as_slice().try_into() { + Ok(bytes) => u32::from_le_bytes(bytes), + Err(_) => { + tracing::warn!("Malformed {PROP_ADDRESS_INDEX} for transparent input {i}"); + return None; + } + }; + + let Some(scope) = decode_key_scope(scope_u32) else { + tracing::warn!("Invalid scope {scope_u32} for transparent input {i}"); + return None; + }; + + let addr_idx = NonHardenedChildIndex::from_index(addr_idx_u32).or_else(|| { + tracing::warn!( + "Invalid address index {addr_idx_u32} for transparent input {i}" + ); + None + })?; + + Some((scope, addr_idx)) + }) + .collect(); + + // Count shielded inputs before the Signer consumes the PCZT. + let sapling_count = pczt.sapling().spends().len(); + let orchard_count = pczt.orchard().actions().len(); + + // Fetch the seed last, to avoid a keystore decryption if unnecessary. + let seed = keystore + .decrypt_seed(&seed_fp) + .await + .map_err(|e| match e.kind() { + crate::error::ErrorKind::Generic if e.to_string() == "Wallet is locked" => { + LegacyCode::WalletUnlockNeeded.with_message(e.to_string()) + } + _ => LegacyCode::Database.with_message(e.to_string()), + })?; + + let usk = UnifiedSpendingKey::from_seed(wallet.params(), seed.expose_secret(), account_idx) + .map_err(|e| { + LegacyCode::InvalidAddressOrKey + .with_message(format!("Failed to derive spending key: {e}")) + })?; + + let mut signer = Signer::new(pczt) + .map_err(|_| LegacyCode::Verify.with_static("Failed to initialize signer"))?; + + // Sign transparent inputs. An input we lack derivation info for, or whose + // key derivation or signature fails, is recorded as unsigned: it may belong + // to a different key. + let mut transparent_signed = 0; + let mut unsigned_transparent = Vec::new(); + for (i, derivation_info) in transparent_derivation_info.iter().enumerate() { + match derivation_info { + Some((scope, addr_idx)) => { + match usk.transparent().derive_secret_key(*scope, *addr_idx) { + Ok(sk) => match signer.sign_transparent(i, &sk) { + Ok(()) => transparent_signed += 1, + Err(_) => unsigned_transparent.push(i), + }, + Err(_) => unsigned_transparent.push(i), + } + } + None => unsigned_transparent.push(i), + } + } + + // Sign Sapling spends. A spend belonging to a different key returns an + // error, which we record as unsigned. + let mut sapling_signed = 0; + let mut unsigned_sapling = Vec::new(); + let sapling_ask = &usk.sapling().expsk.ask; + for i in 0..sapling_count { + match signer.sign_sapling(i, sapling_ask) { + Ok(()) => sapling_signed += 1, + Err(_) => unsigned_sapling.push(i), + } + } + + // Sign Orchard actions. + let mut orchard_signed = 0; + let mut unsigned_orchard = Vec::new(); + let orchard_ask = orchard::keys::SpendAuthorizingKey::from(usk.orchard()); + for i in 0..orchard_count { + match signer.sign_orchard(i, &orchard_ask) { + Ok(()) => orchard_signed += 1, + Err(_) => unsigned_orchard.push(i), + } + } + + if strict.unwrap_or(false) + && (!unsigned_transparent.is_empty() + || !unsigned_sapling.is_empty() + || !unsigned_orchard.is_empty()) + { + return Err(LegacyCode::Verify.with_message(format!( + "Strict mode: {} transparent, {} sapling, {} orchard inputs remain unsigned", + unsigned_transparent.len(), + unsigned_sapling.len(), + unsigned_orchard.len() + ))); + } + + Ok(SignResult { + pczt: encode_pczt_base64(signer.finish())?, + transparent_signed, + sapling_signed, + orchard_signed, + unsigned_transparent, + unsigned_sapling, + unsigned_orchard, + }) +} diff --git a/zallet-core/src/components/json_rpc/methods/z_send_many.rs b/zallet-core/src/components/json_rpc/methods/z_send_many.rs index 56335581..6a36fd40 100644 --- a/zallet-core/src/components/json_rpc/methods/z_send_many.rs +++ b/zallet-core/src/components/json_rpc/methods/z_send_many.rs @@ -5,28 +5,19 @@ use abscissa_core::Application; use jsonrpsee::core::{JsonValue, RpcResult}; use secrecy::ExposeSecret; use serde_json::json; -use zcash_address::unified; use zcash_client_backend::data_api::wallet::SpendingKeys; use zcash_client_backend::proposal::Proposal; use zcash_client_backend::{ data_api::{ Account, - wallet::{ - ConfirmationsPolicy, create_proposed_transactions, - input_selection::{GreedyInputSelector, SpendPolicy}, - propose_transfer, - }, + wallet::{ConfirmationsPolicy, create_proposed_transactions}, }, - fees::{DustOutputPolicy, StandardFeeRule, standard::MultiOutputChangeStrategy}, + fees::StandardFeeRule, wallet::OvkPolicy, }; use zcash_client_sqlite::ReceivedNoteId; use zcash_keys::{address::Address, keys::UnifiedSpendingKey}; use zcash_proofs::prover::LocalTxProver; -use zcash_protocol::{ - PoolType, ShieldedPool, - value::{MAX_MONEY, Zatoshis}, -}; use crate::{ components::{ @@ -35,15 +26,13 @@ use crate::{ json_rpc::{ asyncop::{ContextInfo, OperationId}, payments::{ - AmountParameter, IncompatiblePrivacyPolicy, PrivacyPolicy, SendResult, - broadcast_transactions, build_request, enforce_privacy_policy, - get_account_for_address, + AmountParameter, PrivacyPolicy, SendResult, broadcast_transactions, build_request, + get_account_for_address, propose_and_check, }, server::LegacyCode, }, keystore::KeyStore, }, - fl, prelude::*, }; @@ -131,152 +120,14 @@ pub(crate) async fn call( }; let params = *wallet.params(); - - // TODO: Fetch the real maximums within the account so we can detect correctly. - // https://github.com/zcash/zallet/issues/257 - let mut max_sapling_available = Zatoshis::const_from_u64(MAX_MONEY); - let mut max_orchard_available = Zatoshis::const_from_u64(MAX_MONEY); - - for payment in request.payments().values() { - let value = payment - .amount() - .expect("We set this for every payment above"); - - match Address::try_from_zcash_address(¶ms, payment.recipient_address().clone()) { - Err(e) => return Err(LegacyCode::InvalidParameter.with_message(e.to_string())), - Ok(Address::Transparent(_) | Address::Tex(_)) => { - if !privacy_policy.allow_revealed_recipients() { - return Err(IncompatiblePrivacyPolicy::TransparentRecipient.into()); - } - } - Ok(Address::Sapling(_)) => { - match ( - privacy_policy.allow_revealed_amounts(), - max_sapling_available - value, - ) { - (false, None) => { - return Err(IncompatiblePrivacyPolicy::RevealingSaplingAmount.into()); - } - (false, Some(rest)) => max_sapling_available = rest, - (true, _) => (), - } - } - Ok(Address::Unified(ua)) => { - match ( - privacy_policy.allow_revealed_amounts(), - ( - ua.receiver_types().contains(&unified::Typecode::Orchard), - max_orchard_available - value, - ), - ( - ua.receiver_types().contains(&unified::Typecode::Sapling), - max_sapling_available - value, - ), - ) { - // The preferred receiver is Orchard, and we either allow revealed - // amounts or have sufficient Orchard funds available to avoid it. - (true, (true, _), _) => (), - (false, (true, Some(rest)), _) => max_orchard_available = rest, - - // The preferred receiver is Sapling, and we either allow revealed - // amounts or have sufficient Sapling funds available to avoid it. - (true, _, (true, _)) => (), - (false, _, (true, Some(rest))) => max_sapling_available = rest, - - // We need to reveal something in order to make progress. - _ => { - if privacy_policy.allow_revealed_recipients() { - // Nothing to do here. - } else if privacy_policy.allow_revealed_amounts() { - return Err(IncompatiblePrivacyPolicy::TransparentReceiver.into()); - } else { - return Err(IncompatiblePrivacyPolicy::RevealingReceiverAmounts.into()); - } - } - } - } - } - } - - let change_strategy = MultiOutputChangeStrategy::new( - StandardFeeRule::Zip317, - None, - ShieldedPool::Orchard, - DustOutputPolicy::default(), - APP.config().note_management.split_policy(), - ); - - // TODO: Once `zcash_client_backend` supports spending transparent coins arbitrarily, - // consider using the privacy policy here to avoid selecting incompatible funds. This - // would match what `zcashd` did more closely (though we might instead decide to let - // the selector return its best proposal and then continue to reject afterwards, as we - // currently do). - let input_selector = GreedyInputSelector::new(); - - let proposal = propose_transfer::<_, _, _, _, Infallible>( + let proposal = propose_and_check( wallet.as_mut(), ¶ms, account.id(), - &input_selector, - &change_strategy, request, + privacy_policy, confirmations_policy, - // Zallet does not yet spend transparent funds in a general transfer; `ANY_TADDR` - // spending is rejected above. The default `SpendPolicy` permits every shielded pool - // present in the build and no transparent spending, preserving the prior - // shielded-only behavior. - &SpendPolicy::default(), - // Do not request a specific transaction version; building falls back to the version - // implied by the target height. - None, - ) - // TODO: Map errors to `zcashd` shape. - .map_err(|e| LegacyCode::Wallet.with_message(format!("Failed to propose transaction: {e}")))?; - - enforce_privacy_policy(&proposal, privacy_policy)?; - - let orchard_actions_limit = APP.config().builder.limits.orchard_actions().into(); - for step in proposal.steps() { - let orchard_spends = step - .shielded_inputs() - .iter() - .flat_map(|inputs| inputs.notes()) - .filter(|note| note.note().pool() == ShieldedPool::Orchard) - .count(); - - let orchard_outputs = step - .payment_pools() - .values() - .filter(|pool| pool == &&PoolType::ORCHARD) - .count() - + step - .balance() - .proposed_change() - .iter() - .filter(|change| change.output_pool() == PoolType::ORCHARD) - .count(); - - let orchard_actions = orchard_spends.max(orchard_outputs); - - if orchard_actions > orchard_actions_limit { - let (count, kind) = if orchard_outputs <= orchard_actions_limit { - (orchard_spends, "inputs") - } else if orchard_spends <= orchard_actions_limit { - (orchard_outputs, "outputs") - } else { - (orchard_actions, "actions") - }; - - return Err(LegacyCode::Misc.with_message(fl!( - "err-excess-orchard-actions", - count = count, - kind = kind, - limit = orchard_actions_limit, - config = "-orchardactionlimit=N", - bound = format!("N >= %u"), - ))); - } - } + )?; let derivation = account.source().key_derivation().ok_or_else(|| { LegacyCode::InvalidAddressOrKey diff --git a/zallet-core/src/components/json_rpc/payments.rs b/zallet-core/src/components/json_rpc/payments.rs index b0cfc272..9ee83bfa 100644 --- a/zallet-core/src/components/json_rpc/payments.rs +++ b/zallet-core/src/components/json_rpc/payments.rs @@ -1,23 +1,36 @@ -use std::{collections::HashSet, fmt}; +use std::{collections::HashSet, convert::Infallible, fmt}; use abscissa_core::Application; use jsonrpsee::core::JsonValue; use jsonrpsee::{core::RpcResult, types::ErrorObjectOwned}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use zcash_address::ZcashAddress; +use zcash_address::{ZcashAddress, unified}; use zcash_client_backend::{ - data_api::WalletRead, + data_api::{ + WalletRead, + wallet::{ + ConfirmationsPolicy, + input_selection::{GreedyInputSelector, SpendPolicy}, + propose_transfer, + }, + }, + fees::{DustOutputPolicy, StandardFeeRule, standard::MultiOutputChangeStrategy}, proposal::Proposal, zip321::{Payment, TransactionRequest}, }; -use zcash_client_sqlite::wallet::Account; +use zcash_client_sqlite::{AccountUuid, ReceivedNoteId, wallet::Account}; use zcash_keys::address::Address; -use zcash_protocol::{PoolType, TxId, memo::MemoBytes, value::Zatoshis}; +use zcash_protocol::{ + PoolType, ShieldedPool, TxId, + memo::MemoBytes, + value::{MAX_MONEY, Zatoshis}, +}; use crate::{ components::{chain::Chain, database::DbConnection}, fl, + network::Network, prelude::APP, }; @@ -107,6 +120,166 @@ pub(super) fn build_request(amounts: &[AmountParameter]) -> RpcResult RpcResult> { + // TODO: Fetch the real maximums within the account so we can detect correctly. + // https://github.com/zcash/zallet/issues/257 + let mut max_sapling_available = Zatoshis::const_from_u64(MAX_MONEY); + let mut max_orchard_available = Zatoshis::const_from_u64(MAX_MONEY); + + for payment in request.payments().values() { + let value = payment + .amount() + .expect("Every payment built by `build_request` has an amount"); + + match Address::try_from_zcash_address(params, payment.recipient_address().clone()) { + Err(e) => return Err(LegacyCode::InvalidParameter.with_message(e.to_string())), + Ok(Address::Transparent(_) | Address::Tex(_)) => { + if !privacy_policy.allow_revealed_recipients() { + return Err(IncompatiblePrivacyPolicy::TransparentRecipient.into()); + } + } + Ok(Address::Sapling(_)) => { + match ( + privacy_policy.allow_revealed_amounts(), + max_sapling_available - value, + ) { + (false, None) => { + return Err(IncompatiblePrivacyPolicy::RevealingSaplingAmount.into()); + } + (false, Some(rest)) => max_sapling_available = rest, + (true, _) => (), + } + } + Ok(Address::Unified(ua)) => { + match ( + privacy_policy.allow_revealed_amounts(), + ( + ua.receiver_types().contains(&unified::Typecode::Orchard), + max_orchard_available - value, + ), + ( + ua.receiver_types().contains(&unified::Typecode::Sapling), + max_sapling_available - value, + ), + ) { + // The preferred receiver is Orchard, and we either allow revealed + // amounts or have sufficient Orchard funds available to avoid it. + (true, (true, _), _) => (), + (false, (true, Some(rest)), _) => max_orchard_available = rest, + + // The preferred receiver is Sapling, and we either allow revealed + // amounts or have sufficient Sapling funds available to avoid it. + (true, _, (true, _)) => (), + (false, _, (true, Some(rest))) => max_sapling_available = rest, + + // We need to reveal something in order to make progress. + _ => { + if privacy_policy.allow_revealed_recipients() { + // Nothing to do here. + } else if privacy_policy.allow_revealed_amounts() { + return Err(IncompatiblePrivacyPolicy::TransparentReceiver.into()); + } else { + return Err(IncompatiblePrivacyPolicy::RevealingReceiverAmounts.into()); + } + } + } + } + } + } + + let change_strategy = MultiOutputChangeStrategy::new( + StandardFeeRule::Zip317, + None, + ShieldedPool::Orchard, + DustOutputPolicy::default(), + APP.config().note_management.split_policy(), + ); + + // TODO: Once `zcash_client_backend` supports spending transparent coins arbitrarily, + // consider using the privacy policy here to avoid selecting incompatible funds. + let input_selector = GreedyInputSelector::new(); + + let proposal = propose_transfer::<_, _, _, _, Infallible>( + wallet, + params, + account_id, + &input_selector, + &change_strategy, + request, + confirmations_policy, + // Zallet does not yet spend transparent funds in a general transfer; `ANY_TADDR` + // spending is rejected above. The default `SpendPolicy` permits every shielded pool + // present in the build and no transparent spending, preserving the prior + // shielded-only behavior. + &SpendPolicy::default(), + // Do not request a specific transaction version; building falls back to the version + // implied by the target height. + None, + ) + // TODO: Map errors to `zcashd` shape. + .map_err(|e| LegacyCode::Wallet.with_message(format!("Failed to propose transaction: {e}")))?; + + enforce_privacy_policy(&proposal, privacy_policy)?; + + let orchard_actions_limit = APP.config().builder.limits.orchard_actions().into(); + for step in proposal.steps() { + let orchard_spends = step + .shielded_inputs() + .iter() + .flat_map(|inputs| inputs.notes()) + .filter(|note| note.note().pool() == ShieldedPool::Orchard) + .count(); + + let orchard_outputs = step + .payment_pools() + .values() + .filter(|pool| pool == &&PoolType::ORCHARD) + .count() + + step + .balance() + .proposed_change() + .iter() + .filter(|change| change.output_pool() == PoolType::ORCHARD) + .count(); + + let orchard_actions = orchard_spends.max(orchard_outputs); + + if orchard_actions > orchard_actions_limit { + let (count, kind) = if orchard_outputs <= orchard_actions_limit { + (orchard_spends, "inputs") + } else if orchard_spends <= orchard_actions_limit { + (orchard_outputs, "outputs") + } else { + (orchard_actions, "actions") + }; + + return Err(LegacyCode::Misc.with_message(fl!( + "err-excess-orchard-actions", + count = count, + kind = kind, + limit = orchard_actions_limit, + config = "-orchardactionlimit=N", + bound = format!("N >= %u"), + ))); + } + } + + Ok(proposal) +} + /// A strategy to use for managing privacy when constructing a transaction. /// /// Policy for what information leakage is acceptable in a transaction created via a