Skip to content

rpc: Add JSON-RPC methods for working with PCZTs#541

Open
sellout wants to merge 4 commits into
mainfrom
pczt-rpc-methods
Open

rpc: Add JSON-RPC methods for working with PCZTs#541
sellout wants to merge 4 commits into
mainfrom
pczt-rpc-methods

Conversation

@sellout

@sellout sellout commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Implements the PCZT (Partially Created Zcash Transaction) JSON-RPC methods requested in #99, as the modern, robust replacements for createrawtransaction, fundrawtransaction, and signrawtransaction.

Supersedes #354 (thanks @lamb356 for the initial implementation) — this branch rebuilds that work on current main, addresses all of @nullcopy's review feedback, and fills in the structural gaps.

Closes #99.

Methods

A transaction is assembled by threading a PCZT through these roles:

Method Role(s) Notes
pczt_create propose_transfer + create_pczt_from_proposal Build a funded PCZT from a payment request (≈ createrawtransaction + fundrawtransaction).
pczt_combine Combiner Merge several parties' contributions.
pczt_prove Prover Add the Sapling/Orchard proofs (run off the async runtime; caches the Orchard proving key).
pczt_sign Signer Sign with the wallet's keys.
pczt_extract SpendFinalizer + TransactionExtractor Finalize spends and verify before extracting the final transaction.

The stateless roles (combine/prove/extract) live in the general Rpc trait; create/sign require wallet state.

Notes for review

  • pczt_create vs pczt_fund. feat: Add PCZT RPC methods (issue #99) #354 named this pczt_fund; @nullcopy noted that "fund" reads like adding funds to an existing PCZT. That operation (≈ fundrawtransaction on a partial PCZT) is effectively infeasible at the pinned librustzcash rev: create_pczt_from_proposal IO-finalizes its output (permanently clearing tx_modifiable), there is no Constructor implementation or Builder-from-PCZT path, and no input-selector API for a "cover the remaining value" target. So this method builds a complete PCZT from a payment request in one shot (matching zcash-devtool's pczt create), and pczt_create is the accurate name.
  • Shared logic. pczt_create reuses payments::AmountParameter + build_request (from Extract the shared amount parameter into a reusable payments type #535) and a new shared payments::propose_and_check helper (privacy-policy validation → propose → enforce → Orchard-action-limit) that z_sendmany now also uses, so the two stay in sync.
  • Signing hints. pczt 0.7 exposes no public getter for the native BIP 32 / ZIP 32 derivation metadata, so pczt_create records zallet.v1.* proprietary fields for an offline pczt_sign to read back — a stand-in for the native path until upstream adds accessors.
  • pczt_extract verifies. It runs the spend finalizer and verifies all proofs/signatures (supplying the bundled Sapling verifying keys) before producing the transaction, rather than deferring to the network.

Dependencies

Adds pczt 0.7, pinned to the same librustzcash revision as the other [patch.crates-io] crates, and enables the zcash_client_backend/pczt builder support plus zcash_client_sqlite/serde (required for AccountUuid: Serialize, which create_pczt_from_proposal needs).

Testing

  • Unit tests for the base64 decode size/format limits and the pczt_combine caps.
  • cargo clippy --all-targets -- -D warnings clean; full zallet lib test suite green.
  • ⚠️ No automated end-to-end shielded test. The zcash_client_backend data_api::testing framework can't be used from a downstream crate at the pinned rev — its only concrete store factory (zcash_client_sqlite::testing) is #[cfg(test)]-only, and there is no zcash_client_memory crate at this rev — so the full create → prove → sign → extract path is verified by manual regtest for now. A follow-up could vendor a minimal in-memory store factory to unlock a node-free CI test.

Rebased onto main @ 4fa38e1. The commits on this branch are currently unsigned.

sellout added 4 commits July 11, 2026 00:16
Pin the pczt crate to the same librustzcash revision as the other
patched crates, and enable the pczt roles zallet uses (prover, signer,
spend-finalizer, tx-extractor). Enable the zcash_client_backend `pczt`
feature for create_pczt_from_proposal, and the zcash_client_sqlite
`serde` feature it needs (AccountUuid: Serialize).

Vet the new dependencies for cargo-vet: mark `pczt` as a first-party
librustzcash crate (`audit-as-crates-io = false`, like its siblings), and
add `safe-to-deploy` exemptions for the crates.io deps it pulls in via
`serde_with` (`serde_with`/`serde_with_macros` and
`darling`/`darling_core`/`darling_macro`), across all three workspace
supply-chains.
Add 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 (propose_transfer +
  create_pczt_from_proposal), recording signing hints as proprietary
  fields since pczt 0.7 exposes no public getter for the native
  derivation metadata.
- pczt_combine: merge several parties' contributions (Combiner).
- pczt_prove: add the Sapling and/or Orchard proofs (Prover), off the
  async runtime, caching the Orchard proving key.
- pczt_sign: sign with the wallet's keys (Signer).
- pczt_extract: finalize spends and verify before extracting the final
  transaction (SpendFinalizer + TransactionExtractor).

The stateless roles (combine, prove, extract) and the shared decode
helper (with a 10 MiB input cap and a 20-PCZT combine cap) are available
in every build; create and sign require wallet state. Includes unit
tests for the decode limits and combine caps.
Extract the propose/validate pipeline that z_sendmany and pczt_create had
in common into a shared payments::propose_and_check helper: the
privacy-policy validation loop, propose_transfer with the standard change
strategy and input selector, privacy-policy enforcement, and the
Orchard-action-limit check. Both call sites now share one implementation.

Also have pczt_create reuse the recently-added payments::AmountParameter
type and payments::build_request (upstream #535), instead of its own
AmountParam struct and inline recipient parsing.

And tidy pczt_create's signing hints: drop the unused zallet.v1.network
proprietary field (pczt_sign never reads it) and collapse the two Updater
passes into one chained pass.

Finally, centralize the `zallet.v1.*` signing-hint keys and the key-scope
encoding in `pczt_common`, so the `pczt_create` writer and the `pczt_sign`
reader share one definition rather than duplicating the string keys and the
scope-to-u32 mapping.
@schell

schell commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

there is no zcash_client_memory crate at this rev — so the full create → prove → sign → extract path is verified by manual regtest for now. A follow-up could vendor a minimal in-memory store factory to unlock a node-free CI test

I think this is the biggest deal here, and obviously it's out of scope for this PR, so I've opened zcash/librustzcash#2623 to track that.

Other than that the only thing that jumps out at me is that there's a number of discarded errors and I wonder if it might be good to either roll those into the mapped messages or create variants in the mapped-to error enum? Not a biggy though.

Let me know when it's ready for another pass and I'll give it my stamp 🙇 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rpc: Add JSON-RPC methods for working with PCZTs

2 participants