Repository: zcash/zcash (language: rust)

Issue:
Use latest librustzcash crates (zcash_primitives 0.14, etc.) in zcashd



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/audits.yml b/.github/workflows/audits.yml
index 29b54f5c7fa..e53d3ff6924 100644
--- a/.github/workflows/audits.yml
+++ b/.github/workflows/audits.yml
@@ -17,7 +17,7 @@ jobs:
       - uses: dtolnay/rust-toolchain@stable
         id: toolchain
       - run: rustup override set ${{steps.toolchain.outputs.name}}
-      - run: cargo install cargo-vet --version ~0.8
+      - run: cargo install cargo-vet --version ~0.9
       - run: cargo vet --locked
 
   cargo-deny:
diff --git a/Cargo.lock b/Cargo.lock
index f3bdc2f583a..11e61be297e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -265,12 +265,6 @@ version = "3.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
 
-[[package]]
-name = "byte-slice-cast"
-version = "1.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c"
-
 [[package]]
 name = "byteorder"
 version = "1.5.0"
@@ -540,6 +534,15 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "document-features"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95"
+dependencies = [
+ "litrs",
+]
+
 [[package]]
 name = "ed25519"
 version = "2.2.3"
@@ -583,12 +586,6 @@ dependencies = [
  "byteorder",
 ]
 
-[[package]]
-name = "equivalent"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
-
 [[package]]
 name = "errno"
 version = "0.3.8"
@@ -633,13 +630,10 @@ checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7"
 
 [[package]]
 name = "fixed-hash"
-version = "0.7.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c"
+checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534"
 dependencies = [
- "byteorder",
- "rand",
- "rustc-hex",
  "static_assertions",
 ]
 
@@ -926,26 +920,6 @@ dependencies = [
  "want",
 ]
 
-[[package]]
-name = "impl-codec"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f"
-dependencies = [
- "parity-scale-codec",
-]
-
-[[package]]
-name = "impl-trait-for-tuples"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
 [[package]]
 name = "incrementalmerkletree"
 version = "0.5.0"
@@ -966,16 +940,6 @@ dependencies = [
  "hashbrown 0.12.3",
 ]
 
-[[package]]
-name = "indexmap"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
-dependencies = [
- "equivalent",
- "hashbrown 0.14.3",
-]
-
 [[package]]
 name = "inout"
 version = "0.1.3"
@@ -1098,6 +1062,8 @@ dependencies = [
  "rand",
  "rand_core",
  "rayon",
+ "redjubjub",
+ "sapling-crypto",
  "secp256k1",
  "secrecy",
  "serde",
@@ -1137,6 +1103,12 @@ version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
 
+[[package]]
+name = "litrs"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
+
 [[package]]
 name = "log"
 version = "0.4.20"
@@ -1205,7 +1177,7 @@ checksum = "1d4fa7ce7c4862db464a37b0b31d89bca874562f034bd7993895572783d02950"
 dependencies = [
  "base64",
  "hyper",
- "indexmap 1.9.3",
+ "indexmap",
  "ipnet",
  "metrics",
  "metrics-util",
@@ -1369,9 +1341,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
 name = "orchard"
-version = "0.6.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d31e68534df32024dcc89a8390ec6d7bef65edd87d91b45cfb481a2eb2d77c5"
+checksum = "1fb255c3ffdccd3c84fe9ebed72aef64fdc72e6a3e4180dd411002d47abaad42"
 dependencies = [
  "aes",
  "bitvec",
@@ -1394,6 +1366,8 @@ dependencies = [
  "subtle",
  "tracing",
  "zcash_note_encryption",
+ "zcash_spec",
+ "zip32",
 ]
 
 [[package]]
@@ -1411,32 +1385,6 @@ dependencies = [
  "group",
 ]
 
-[[package]]
-name = "parity-scale-codec"
-version = "3.6.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "881331e34fa842a2fb61cc2db9643a8fedc615e47cfcc52597d1af0db9a7e8fe"
-dependencies = [
- "arrayvec",
- "bitvec",
- "byte-slice-cast",
- "impl-trait-for-tuples",
- "parity-scale-codec-derive",
- "serde",
-]
-
-[[package]]
-name = "parity-scale-codec-derive"
-version = "3.6.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b"
-dependencies = [
- "proc-macro-crate",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
 [[package]]
 name = "password-hash"
 version = "0.3.2"
@@ -1570,25 +1518,14 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 
 [[package]]
 name = "primitive-types"
-version = "0.11.1"
+version = "0.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e28720988bff275df1f51b171e1b2a18c30d194c4d2b61defdacecd625a5d94a"
+checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2"
 dependencies = [
  "fixed-hash",
- "impl-codec",
  "uint",
 ]
 
-[[package]]
-name = "proc-macro-crate"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a"
-dependencies = [
- "toml_datetime",
- "toml_edit",
-]
-
 [[package]]
 name = "proc-macro2"
 version = "1.0.74"
@@ -1848,12 +1785,6 @@ version = "0.1.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
 
-[[package]]
-name = "rustc-hex"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6"
-
 [[package]]
 name = "rustc_version"
 version = "0.4.0"
@@ -1894,6 +1825,39 @@ version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
 
+[[package]]
+name = "sapling-crypto"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d183012062dfdde85f7e3e758328fcf6e9846d8dd3fce35b04d0efcb6677b0e0"
+dependencies = [
+ "aes",
+ "bellman",
+ "bitvec",
+ "blake2b_simd",
+ "blake2s_simd",
+ "bls12_381",
+ "byteorder",
+ "document-features",
+ "ff",
+ "fpe",
+ "group",
+ "hex",
+ "incrementalmerkletree",
+ "jubjub",
+ "lazy_static",
+ "memuse",
+ "proptest",
+ "rand",
+ "rand_core",
+ "redjubjub",
+ "subtle",
+ "tracing",
+ "zcash_note_encryption",
+ "zcash_spec",
+ "zip32",
+]
+
 [[package]]
 name = "secp256k1"
 version = "0.26.0"
@@ -2179,23 +2143,6 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
-[[package]]
-name = "toml_datetime"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
-
-[[package]]
-name = "toml_edit"
-version = "0.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
-dependencies = [
- "indexmap 2.1.0",
- "toml_datetime",
- "winnow",
-]
-
 [[package]]
 name = "tower-service"
 version = "0.3.2"
@@ -2640,15 +2587,6 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
 
-[[package]]
-name = "winnow"
-version = "0.5.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c"
-dependencies = [
- "memchr",
-]
-
 [[package]]
 name = "wyz"
 version = "0.5.1"
@@ -2666,9 +2604,9 @@ checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
 
 [[package]]
 name = "zcash_address"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8944af5c206cf2e37020ad54618e1825501b98548d35a638b73e0ec5762df8d5"
+checksum = "bce173f1d9ed4f806e310bc3a873301531e7a6dc209928584d6404e3f8228ef4"
 dependencies = [
  "bech32",
  "bs58",
@@ -2688,9 +2626,9 @@ dependencies = [
 
 [[package]]
 name = "zcash_history"
-version = "0.3.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb611a28a4e13ac715ee712f4344d6b279b767daf6345dafefb2c4bf582b6679"
+checksum = "2fde17bf53792f9c756b313730da14880257d7661b5bfc69d0571c3a7c11a76d"
 dependencies = [
  "blake2b_simd",
  "byteorder",
@@ -2712,17 +2650,15 @@ dependencies = [
 
 [[package]]
 name = "zcash_primitives"
-version = "0.13.0"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d17e4c94ca8d69d2fcf2be97522da5732a580eb2125cda3b150761952f8df8e6"
+checksum = "9070e084570bb78aed4f8d71fd6254492e62c87a5d01e084183980e98117092d"
 dependencies = [
  "aes",
  "bip0039",
- "bitvec",
  "blake2b_simd",
- "blake2s_simd",
- "bls12_381",
  "byteorder",
+ "document-features",
  "equihash",
  "ff",
  "fpe",
@@ -2731,31 +2667,36 @@ dependencies = [
  "hex",
  "incrementalmerkletree",
  "jubjub",
- "lazy_static",
  "memuse",
  "nonempty",
  "orchard",
  "proptest",
  "rand",
  "rand_core",
+ "redjubjub",
  "ripemd",
+ "sapling-crypto",
  "secp256k1",
  "sha2",
  "subtle",
+ "tracing",
  "zcash_address",
  "zcash_encoding",
  "zcash_note_encryption",
+ "zcash_spec",
+ "zip32",
 ]
 
 [[package]]
 name = "zcash_proofs"
-version = "0.13.0"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df0c99f65a840ff256c106b28d67d702d9759d206112473d4982c92003262406"
+checksum = "b8a02eb1f151d9b9a6e16408d2c55ff440bd2fb232b7377277146d0fa2df9bc8"
 dependencies = [
  "bellman",
  "blake2b_simd",
  "bls12_381",
+ "document-features",
  "group",
  "home",
  "jubjub",
@@ -2763,11 +2704,21 @@ dependencies = [
  "lazy_static",
  "rand_core",
  "redjubjub",
+ "sapling-crypto",
  "tracing",
  "xdg",
  "zcash_primitives",
 ]
 
+[[package]]
+name = "zcash_spec"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7a3bf58b673cb3dacd8ae09ba345998923a197ab0da70d6239d8e8838949e9b"
+dependencies = [
+ "blake2b_simd",
+]
+
 [[package]]
 name = "zerocopy"
 version = "0.7.32"
@@ -2807,3 +2758,14 @@ dependencies = [
  "quote",
  "syn 2.0.46",
 ]
+
+[[package]]
+name = "zip32"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d724a63be4dfb50b7f3617e542984e22e4b4a5b8ca5de91f55613152885e6b22"
+dependencies = [
+ "blake2b_simd",
+ "memuse",
+ "subtle",
+]
diff --git a/Cargo.toml b/Cargo.toml
index aa4da879110..69d11c381e3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -52,19 +52,21 @@ libc = "0.2"
 jubjub = "0.10"
 memuse = "0.2"
 nonempty = "0.7"
-orchard = "0.6"
+orchard = "0.7"
+sapling = { package = "sapling-crypto", version = "0.1", features = ["temporary-zcashd"] }
 secp256k1 = "0.26"
 subtle = "2.2"
 rand_core = "0.6"
+redjubjub = "0.7"
 tracing = "0.1"
 tracing-core = "0.1"
 tracing-appender = "0.2"
 zcash_address = "0.3"
 zcash_encoding = "0.2"
-zcash_history = "0.3"
+zcash_history = "0.4"
 zcash_note_encryption = "0.4"
-zcash_primitives = { version = "0.13", features = ["temporary-zcashd", "transparent-inputs"] }
-zcash_proofs = { version = "0.13", features = ["directories"] }
+zcash_primitives = { version = "0.14", features = ["temporary-zcashd", "transparent-inputs"] }
+zcash_proofs = { version = "0.14", features = ["directories"] }
 ed25519-zebra = "4"
 zeroize = "1.4.2"
 wagyu-zcash-parameters = "0.2"
@@ -109,7 +111,7 @@ time = { version = "0.3", features = ["formatting", "macros"] }
 [dev-dependencies]
 incrementalmerkletree = { version = "0.5", features = ["test-dependencies"] }
 proptest = "1.0.0"
-zcash_primitives = { version = "0.13", features = ["temporary-zcashd", "transparent-inputs", "test-dependencies"] }
+zcash_primitives = { version = "0.14", features = ["temporary-zcashd", "transparent-inputs", "test-dependencies"] }
 
 [dependencies.tracing-subscriber]
 version = "0.3"
diff --git a/qa/supply-chain/audits.toml b/qa/supply-chain/audits.toml
index c9de9c803b6..1816997ec78 100644
--- a/qa/supply-chain/audits.toml
+++ b/qa/supply-chain/audits.toml
@@ -2634,6 +2634,12 @@ user-id = 1244 # ebfull
 start = "2022-10-19"
 end = "2024-09-21"
 
+[[trusted.sapling-crypto]]
+criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"]
+user-id = 6289 # str4d
+start = "2024-01-26"
+end = "2025-03-18"
+
 [[trusted.windows-sys]]
 criteria = "safe-to-deploy"
 user-id = 64539 # Kenny Kerr (kennykerr)
@@ -2694,6 +2700,12 @@ user-id = 1244 # ebfull
 start = "2022-10-19"
 end = "2024-09-21"
 
+[[trusted.zcash_address]]
+criteria = "safe-to-deploy"
+user-id = 6289 # str4d
+start = "2021-03-07"
+end = "2025-03-18"
+
 [[trusted.zcash_encoding]]
 criteria = "safe-to-deploy"
 user-id = 1244 # ebfull
@@ -2706,6 +2718,12 @@ user-id = 1244 # ebfull
 start = "2020-03-04"
 end = "2024-09-21"
 
+[[trusted.zcash_history]]
+criteria = "safe-to-deploy"
+user-id = 6289 # str4d
+start = "2024-03-01"
+end = "2025-03-18"
+
 [[trusted.zcash_note_encryption]]
 criteria = ["safe-to-deploy", "crypto-reviewed"]
 user-id = 169181 # Kris Nuttycombe (nuttycom)
@@ -2729,3 +2747,15 @@ criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"]
 user-id = 6289 # str4d
 start = "2021-03-26"
 end = "2024-09-21"
+
+[[trusted.zcash_spec]]
+criteria = ["safe-to-deploy", "crypto-reviewed", "license-reviewed"]
+user-id = 6289 # str4d
+start = "2023-12-07"
+end = "2025-03-18"
+
+[[trusted.zip32]]
+criteria = "safe-to-deploy"
+user-id = 6289 # str4d
+start = "2023-12-06"
+end = "2025-03-18"
diff --git a/qa/supply-chain/config.toml b/qa/supply-chain/config.toml
index bd8dc22f74c..859eaea43bb 100644
--- a/qa/supply-chain/config.toml
+++ b/qa/supply-chain/config.toml
@@ -2,7 +2,7 @@
 # cargo-vet config file
 
 [cargo-vet]
-version = "0.8"
+version = "0.9"
 
 [imports.bytecode-alliance]
 url = "https://raw.githubusercontent.com/bytecodealliance/wasmtime/main/supply-chain/audits.toml"
@@ -91,10 +91,6 @@ criteria = "safe-to-deploy"
 version = "0.5.0"
 criteria = "safe-to-deploy"
 
-[[exemptions.byte-slice-cast]]
-version = "1.2.1"
-criteria = "safe-to-deploy"
-
 [[exemptions.byteorder]]
 version = "1.4.3"
 criteria = "safe-to-deploy"
@@ -195,16 +191,12 @@ criteria = "safe-to-deploy"
 version = "3.0.0"
 criteria = "safe-to-deploy"
 
-[[exemptions.equivalent]]
-version = "1.0.0"
-criteria = "safe-to-deploy"
-
 [[exemptions.ff]]
 version = "0.12.0"
 criteria = "safe-to-deploy"
 
 [[exemptions.fixed-hash]]
-version = "0.7.0"
+version = "0.8.0"
 criteria = "safe-to-deploy"
 
 [[exemptions.fpe]]
@@ -279,22 +271,10 @@ criteria = "safe-to-deploy"
 version = "0.14.25"
 criteria = "safe-to-deploy"
 
-[[exemptions.impl-codec]]
-version = "0.6.0"
-criteria = "safe-to-deploy"
-
-[[exemptions.impl-trait-for-tuples]]
-version = "0.2.2"
-criteria = "safe-to-deploy"
-
 [[exemptions.indexmap]]
 version = "1.8.1"
 criteria = "safe-to-deploy"
 
-[[exemptions.indexmap]]
-version = "2.0.0"
-criteria = "safe-to-deploy"
-
 [[exemptions.ipnet]]
 version = "2.5.0"
 criteria = "safe-to-deploy"
@@ -363,10 +343,6 @@ criteria = "safe-to-deploy"
 version = "0.26.1"
 criteria = "safe-to-deploy"
 
-[[exemptions.nom]]
-version = "7.1.1"
-criteria = "safe-to-deploy"
-
 [[exemptions.nonempty]]
 version = "0.7.0"
 criteria = "safe-to-deploy"
@@ -387,14 +363,6 @@ criteria = "safe-to-deploy"
 version = "0.22.0"
 criteria = "safe-to-deploy"
 
-[[exemptions.parity-scale-codec]]
-version = "3.6.1"
-criteria = "safe-to-deploy"
-
-[[exemptions.parity-scale-codec-derive]]
-version = "3.6.5"
-criteria = "safe-to-deploy"
-
 [[exemptions.password-hash]]
 version = "0.3.2"
 criteria = "safe-to-deploy"
@@ -444,11 +412,7 @@ version = "0.2.16"
 criteria = "safe-to-deploy"
 
 [[exemptions.primitive-types]]
-version = "0.11.1"
-criteria = "safe-to-deploy"
-
-[[exemptions.proc-macro-crate]]
-version = "1.2.1"
+version = "0.12.2"
 criteria = "safe-to-deploy"
 
 [[exemptions.proptest]]
@@ -515,10 +479,6 @@ criteria = "safe-to-deploy"
 version = "0.1.3"
 criteria = "safe-to-deploy"
 
-[[exemptions.rustc-hex]]
-version = "2.1.0"
-criteria = "safe-to-deploy"
-
 [[exemptions.rustix]]
 version = "0.38.28"
 criteria = "safe-to-deploy"
@@ -579,10 +539,6 @@ criteria = "safe-to-deploy"
 version = "0.7.3"
 criteria = "safe-to-deploy"
 
-[[exemptions.static_assertions]]
-version = "1.1.0"
-criteria = "safe-to-deploy"
-
 [[exemptions.subtle]]
 version = "2.4.1"
 criteria = "safe-to-deploy"
@@ -611,10 +567,6 @@ criteria = "safe-to-deploy"
 version = "1.35.0"
 criteria = "safe-to-deploy"
 
-[[exemptions.toml_edit]]
-version = "0.19.15"
-criteria = "safe-to-deploy"
-
 [[exemptions.tower-service]]
 version = "0.3.2"
 criteria = "safe-to-deploy"
@@ -691,10 +643,6 @@ criteria = "safe-to-deploy"
 version = "0.4.0"
 criteria = "safe-to-deploy"
 
-[[exemptions.winnow]]
-version = "0.5.31"
-criteria = "safe-to-deploy"
-
 [[exemptions.wyz]]
 version = "0.5.0"
 criteria = "safe-to-deploy"
diff --git a/qa/supply-chain/imports.lock b/qa/supply-chain/imports.lock
index b1cc37600fd..2a14cfbe3be 100644
--- a/qa/supply-chain/imports.lock
+++ b/qa/supply-chain/imports.lock
@@ -52,8 +52,14 @@ user-id = 6289
 user-login = "str4d"
 
 [[publisher.orchard]]
-version = "0.6.0"
-when = "2023-09-08"
+version = "0.7.1"
+when = "2024-02-29"
+user-id = 6289
+user-login = "str4d"
+
+[[publisher.sapling-crypto]]
+version = "0.1.1"
+when = "2024-02-15"
 user-id = 6289
 user-login = "str4d"
 
@@ -184,10 +190,10 @@ user-login = "kennykerr"
 user-name = "Kenny Kerr"
 
 [[publisher.zcash_address]]
-version = "0.3.0"
-when = "2023-06-06"
-user-id = 1244
-user-login = "ebfull"
+version = "0.3.1"
+when = "2024-01-12"
+user-id = 6289
+user-login = "str4d"
 
 [[publisher.zcash_encoding]]
 version = "0.2.0"
@@ -196,10 +202,10 @@ user-id = 1244
 user-login = "ebfull"
 
 [[publisher.zcash_history]]
-version = "0.3.0"
-when = "2022-05-11"
-user-id = 1244
-user-login = "ebfull"
+version = "0.4.0"
+when = "2024-03-01"
+user-id = 6289
+user-login = "str4d"
 
 [[publisher.zcash_note_encryption]]
 version = "0.4.0"
@@ -209,14 +215,26 @@ user-login = "nuttycom"
 user-name = "Kris Nuttycombe"
 
 [[publisher.zcash_primitives]]
-version = "0.13.0"
-when = "2023-09-25"
+version = "0.14.0"
+when = "2024-03-01"
 user-id = 6289
 user-login = "str4d"
 
 [[publisher.zcash_proofs]]
-version = "0.13.0"
-when = "2023-09-25"
+version = "0.14.0"
+when = "2024-03-01"
+user-id = 6289
+user-login = "str4d"
+
+[[publisher.zcash_spec]]
+version = "0.1.0"
+when = "2023-12-07"
+user-id = 6289
+user-login = "str4d"
+
+[[publisher.zip32]]
+version = "0.1.0"
+when = "2023-12-06"
 user-id = 6289
 user-login = "str4d"
 
@@ -466,6 +484,12 @@ criteria = "safe-to-deploy"
 version = "0.1.4"
 notes = "I always really enjoy reading eliza's code, she left perfect comments at every use of unsafe."
 
+[[audits.bytecode-alliance.audits.static_assertions]]
+who = "Andrew Brown <andrew.brown@intel.com>"
+criteria = "safe-to-deploy"
+version = "1.1.0"
+notes = "No dependencies and completely a compile-time crate as advertised. Uses `unsafe` in one module as a compile-time check only: `mem::transmute` and `ptr::write` are wrapped in an impossible-to-run closure."
+
 [[audits.bytecode-alliance.audits.tempfile]]
 who = "Pat Hickey <phickey@fastly.com>"
 criteria = "safe-to-deploy"
@@ -553,12 +577,6 @@ criteria = "safe-to-deploy"
 version = "1.0.40"
 notes = "Found no unsafe or ambient capabilities used"
 
-[[audits.embark-studios.audits.toml_datetime]]
-who = "Johan Andersson <opensource@embark-studios.com>"
-criteria = "safe-to-deploy"
-delta = "0.6.1 -> 0.6.2"
-notes = "No notable changes"
-
 [[audits.embark-studios.audits.valuable]]
 who = "Johan Andersson <opensource@embark-studios.com>"
 criteria = "safe-to-deploy"
@@ -597,6 +615,15 @@ are made about the safety of either of those libraries. :)
 """
 aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT"
 
+[[audits.google.audits.nom]]
+who = "danakj@chromium.org"
+criteria = "safe-to-deploy"
+version = "7.1.3"
+notes = """
+Reviewed in https://chromium-review.googlesource.com/c/chromium/src/+/5046153
+"""
+aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT"
+
 [[audits.google.audits.pin-project-lite]]
 who = "David Koloski <dkoloski@google.com>"
 criteria = "safe-to-deploy"
@@ -1034,6 +1061,12 @@ criteria = "safe-to-deploy"
 delta = "0.10.3 -> 0.10.6"
 aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
 
+[[audits.mozilla.audits.document-features]]
+who = "Erich Gubler <erichdongubler@gmail.com>"
+criteria = "safe-to-deploy"
+version = "0.2.8"
+aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
+
 [[audits.mozilla.audits.either]]
 who = "Mike Hommey <mh+mozilla@glandium.org>"
 criteria = "safe-to-deploy"
@@ -1109,6 +1142,12 @@ version = "1.4.0"
 notes = "I have read over the macros, and audited the unsafe code."
 aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml"
 
+[[audits.mozilla.audits.litrs]]
+who = "Erich Gubler <erichdongubler@gmail.com>"
+criteria = "safe-to-deploy"
+version = "0.4.1"
+aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
+
 [[audits.mozilla.audits.log]]
 who = "Mike Hommey <mh+mozilla@glandium.org>"
 criteria = "safe-to-deploy"
@@ -1128,12 +1167,6 @@ criteria = "safe-to-deploy"
 version = "0.4.1"
 aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
 
-[[audits.mozilla.audits.nom]]
-who = "Mike Hommey <mh+mozilla@glandium.org>"
-criteria = "safe-to-deploy"
-delta = "7.1.1 -> 7.1.3"
-aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
-
 [[audits.mozilla.audits.num-bigint]]
 who = "Josh Stone <jistone@redhat.com>"
 criteria = "safe-to-deploy"
diff --git a/src/miner.cpp b/src/miner.cpp
index 5c56d5d5240..4c9e4250d75 100644
--- a/src/miner.cpp
+++ b/src/miner.cpp
@@ -173,7 +173,7 @@ class AddOutputsToCoinbaseTxAndSign
 
     void ComputeBindingSig(rust::Box<sapling::Builder> saplingBuilder, std::optional<orchard::UnauthorizedBundle> orchardBundle) const {
         auto consensusBranchId = CurrentEpochBranchId(nHeight, chainparams.GetConsensus());
-        auto saplingBundle = sapling::build_bundle(std::move(saplingBuilder), nHeight);
+        auto saplingBundle = sapling::build_bundle(std::move(saplingBuilder));
 
         // Empty output script.
         uint256 dataToBeSigned;
@@ -213,13 +213,14 @@ class AddOutputsToCoinbaseTxAndSign
 
     // Create Orchard output
     void operator()(const libzcash::OrchardRawAddress &to) const {
-        auto saplingBuilder = sapling::new_builder(*chainparams.RustNetwork(), nHeight);
+        std::array<uint8_t, 32> saplingAnchor;
+        auto saplingBuilder = sapling::new_builder(*chainparams.RustNetwork(), nHeight, saplingAnchor, true);
 
         // `enableSpends` must be set to `false` for coinbase transactions. This
         // means the Orchard anchor is unconstrained, so we set it to the empty
         // tree root via a null (all zeroes) uint256.
         uint256 orchardAnchor;
-        auto builder = orchard::Builder(false, true, orchardAnchor);
+        auto builder = orchard::Builder(true, orchardAnchor);
 
         // Shielded coinbase outputs must be recoverable with an all-zeroes ovk.
         uint256 ovk;
@@ -249,7 +250,8 @@ class AddOutputsToCoinbaseTxAndSign
 
     // Create Sapling output
     void operator()(const libzcash::SaplingPaymentAddress &pa) const {
-        auto saplingBuilder = sapling::new_builder(*chainparams.RustNetwork(), nHeight);
+        std::array<uint8_t, 32> saplingAnchor;
+        auto saplingBuilder = sapling::new_builder(*chainparams.RustNetwork(), nHeight, saplingAnchor, true);
 
         auto miner_reward = SetFoundersRewardAndGetMinerValue(*saplingBuilder);
 
@@ -265,7 +267,8 @@ class AddOutputsToCoinbaseTxAndSign
     // Create transparent output
     void operator()(const boost::shared_ptr<CReserveScript> &coinbaseScript) const {
         // Add the FR output and fetch the miner's output value.
-        auto saplingBuilder = sapling::new_builder(*chainparams.RustNetwork(), nHeight);
+        std::array<uint8_t, 32> saplingAnchor;
+        auto saplingBuilder = sapling::new_builder(*chainparams.RustNetwork(), nHeight, saplingAnchor, true);
 
         // Miner output will be vout[0]; Founders' Reward & funding stream outputs
         // will follow.
diff --git a/src/rust/bin/inspect/context.rs b/src/rust/bin/inspect/context.rs
index 0bd240d0e39..f7d9a56dae7 100644
--- a/src/rust/bin/inspect/context.rs
+++ b/src/rust/bin/inspect/context.rs
@@ -9,7 +9,7 @@ use serde::{
 use zcash_primitives::{
     consensus::Network,
     legacy::Script,
-    transaction::components::{transparent, Amount, TxOut},
+    transaction::components::{amount::NonNegativeAmount, transparent, TxOut},
     zip32::AccountId,
 };
 
@@ -67,7 +67,9 @@ impl<'de> Visitor<'de> for JsonAccountIdVisitor {
     {
         u32::try_from(v)
             .map_err(|_| E::custom(format!("u32 out of range: {}", v)))
-            .map(AccountId::from)
+            .and_then(|a| {
+                AccountId::try_from(a).map_err(|e| E::custom(format!("AccountId invalid: {}", e)))
+            })
             .map(JsonAccountId)
     }
 
@@ -77,7 +79,9 @@ impl<'de> Visitor<'de> for JsonAccountIdVisitor {
     {
         u32::try_from(v)
             .map_err(|_| E::custom(format!("u32 out of range: {}", v)))
-            .map(AccountId::from)
+            .and_then(|a| {
+                AccountId::try_from(a).map_err(|e| E::custom(format!("AccountId invalid: {}", e)))
+            })
             .map(JsonAccountId)
     }
 
@@ -87,7 +91,9 @@ impl<'de> Visitor<'de> for JsonAccountIdVisitor {
     {
         u32::try_from(v)
             .map_err(|_| E::custom(format!("u32 out of range: {}", v)))
-            .map(AccountId::from)
+            .and_then(|a| {
+                AccountId::try_from(a).map_err(|e| E::custom(format!("AccountId invalid: {}", e)))
+            })
             .map(JsonAccountId)
     }
 
@@ -97,7 +103,9 @@ impl<'de> Visitor<'de> for JsonAccountIdVisitor {
     {
         u32::try_from(v)
             .map_err(|_| E::custom(format!("u32 out of range: {}", v)))
-            .map(AccountId::from)
+            .and_then(|a| {
+                AccountId::try_from(a).map_err(|e| E::custom(format!("AccountId invalid: {}", e)))
+            })
             .map(JsonAccountId)
     }
 }
@@ -162,7 +170,7 @@ impl fmt::Display for ZUint256 {
 }
 
 #[derive(Clone, Debug)]
-struct ZOutputValue(Amount);
+struct ZOutputValue(NonNegativeAmount);
 
 struct ZOutputValueVisitor;
 
@@ -177,9 +185,11 @@ impl<'de> Visitor<'de> for ZOutputValueVisitor {
     where
         E: serde::de::Error,
     {
-        Amount::from_u64(v).map(ZOutputValue).map_err(|()| {
-            serde::de::Error::invalid_type(Unexpected::Unsigned(v), &"a valid zatoshi amount")
-        })
+        NonNegativeAmount::from_u64(v)
+            .map(ZOutputValue)
+            .map_err(|()| {
+                serde::de::Error::invalid_type(Unexpected::Unsigned(v), &"a valid zatoshi amount")
+            })
     }
 }
 
diff --git a/src/rust/bin/inspect/keys.rs b/src/rust/bin/inspect/keys.rs
index 152f95cfdc3..a0b188e4521 100644
--- a/src/rust/bin/inspect/keys.rs
+++ b/src/rust/bin/inspect/keys.rs
@@ -36,7 +36,7 @@ pub(crate) fn inspect_mnemonic(
             let orchard_fvk = match orchard::keys::SpendingKey::from_zip32_seed(
                 &seed,
                 network.coin_type(),
-                account.into(),
+                account,
             ) {
                 Ok(sk) => Some(orchard::keys::FullViewingKey::from(&sk)),
                 Err(e) => {
@@ -49,13 +49,13 @@ pub(crate) fn inspect_mnemonic(
             };
 
             eprintln!("   - Sapling:");
-            let sapling_master = zip32::ExtendedSpendingKey::master(&seed);
-            let sapling_extsk = zip32::ExtendedSpendingKey::from_path(
+            let sapling_master = sapling::zip32::ExtendedSpendingKey::master(&seed);
+            let sapling_extsk = sapling::zip32::ExtendedSpendingKey::from_path(
                 &sapling_master,
                 &[
-                    zip32::ChildIndex::Hardened(32),
-                    zip32::ChildIndex::Hardened(network.coin_type()),
-                    zip32::ChildIndex::Hardened(account.into()),
+                    zip32::ChildIndex::hardened(32),
+                    zip32::ChildIndex::hardened(network.coin_type()),
+                    account.into(),
                 ],
             );
             #[allow(deprecated)]
@@ -106,8 +106,8 @@ pub(crate) fn inspect_mnemonic(
                         Ok(addr) => eprintln!(
                             "     - Default address: {}",
                             match addr {
-                                TransparentAddress::PublicKey(data) => ZcashAddress::from_transparent_p2pkh(addr_net, data),
-                                TransparentAddress::Script(_) => unreachable!(),
+                                TransparentAddress::PublicKeyHash(data) => ZcashAddress::from_transparent_p2pkh(addr_net, data),
+                                TransparentAddress::ScriptHash(_) => unreachable!(),
                             }.encode(),
                         ),
                         Err(e) => eprintln!(
diff --git a/src/rust/bin/inspect/main.rs b/src/rust/bin/inspect/main.rs
index e36ef895712..158d6ba07cb 100644
--- a/src/rust/bin/inspect/main.rs
+++ b/src/rust/bin/inspect/main.rs
@@ -98,8 +98,9 @@ fn inspect_bytes(bytes: Vec<u8>, context: Option<Context>) {
         block::inspect(&block, context);
     } else if let Some(header) = complete(&bytes, |r| BlockHeader::read(r)) {
         block::inspect_header(&header, context);
-    } else if let Some(tx) = complete(&bytes, |r| Transaction::read(r, BranchId::Sprout)) {
+    } else if let Some(tx) = complete(&bytes, |r| Transaction::read(r, BranchId::Nu5)) {
         // TODO: Take the branch ID used above from the context if present.
+        // https://github.com/zcash/zcash/issues/6831
         transaction::inspect(tx, context);
     } else {
         // It's not a known variable-length format. check fixed-length data formats.
diff --git a/src/rust/bin/inspect/transaction.rs b/src/rust/bin/inspect/transaction.rs
index 56af769cd6c..4e9308c00b8 100644
--- a/src/rust/bin/inspect/transaction.rs
+++ b/src/rust/bin/inspect/transaction.rs
@@ -6,6 +6,7 @@ use std::{
 use bellman::groth16;
 use group::GroupEncoding;
 use orchard::note_encryption::OrchardDomain;
+use sapling::{note_encryption::SaplingDomain, SaplingVerificationContext};
 use secp256k1::{Secp256k1, VerifyOnly};
 use zcash_address::{
     unified::{self, Encoding},
@@ -14,18 +15,16 @@ use zcash_address::{
 use zcash_note_encryption::try_output_recovery_with_ovk;
 #[allow(deprecated)]
 use zcash_primitives::{
-    consensus::BlockHeight,
+    consensus::{sapling_zip212_enforcement, BlockHeight},
     legacy::{keys::pubkey_to_address, Script, TransparentAddress},
     memo::{Memo, MemoBytes},
-    sapling::note_encryption::SaplingDomain,
     transaction::{
-        components::{sapling, transparent, Amount},
+        components::{amount::NonNegativeAmount, transparent},
         sighash::{signature_hash, SignableInput, TransparentAuthorizingContext},
         txid::TxIdDigester,
         Authorization, Transaction, TransactionData, TxId, TxVersion,
     },
 };
-use zcash_proofs::sapling::SaplingVerificationContext;
 
 use crate::{
     context::{Context, ZTxOut},
@@ -106,7 +105,7 @@ impl transparent::Authorization for TransparentAuth {
 }
 
 impl TransparentAuthorizingContext for TransparentAuth {
-    fn input_amounts(&self) -> Vec<Amount> {
+    fn input_amounts(&self) -> Vec<NonNegativeAmount> {
         self.all_prev_outputs
             .iter()
             .map(|prevout| prevout.value)
@@ -143,7 +142,7 @@ pub(crate) struct PrecomputedAuth;
 
 impl Authorization for PrecomputedAuth {
     type TransparentAuth = TransparentAuth;
-    type SaplingAuth = sapling::Authorized;
+    type SaplingAuth = sapling::bundle::Authorized;
     type OrchardAuth = orchard::bundle::Authorized;
 }
 
@@ -231,7 +230,7 @@ pub(crate) fn inspect(tx: Transaction, context: Option<Context>) {
                         txin.prevout.n()
                     );
                     match coin.recipient_address() {
-                        Some(addr @ TransparentAddress::PublicKey(_)) => {
+                        Some(addr @ TransparentAddress::PublicKeyHash(_)) => {
                             // Format is [sig_and_type_len] || sig || [hash_type] || [pubkey_len] || pubkey
                             // where [x] encodes a single byte.
                             let sig_and_type_len = txin.script_sig.0.first().map(|l| *l as usize);
@@ -311,7 +310,7 @@ pub(crate) fn inspect(tx: Transaction, context: Option<Context>) {
                             }
                         }
                         // TODO: Check P2SH structure.
-                        Some(TransparentAddress::Script(_)) => {
+                        Some(TransparentAddress::ScriptHash(_)) => {
                             eprintln!("  🔎 \"transparentcoins\"[{}] is a P2SH coin.", i);
                         }
                         // TODO: Check arbitrary scripts.
@@ -376,7 +375,7 @@ pub(crate) fn inspect(tx: Transaction, context: Option<Context>) {
         assert!(!(bundle.shielded_spends().is_empty() && bundle.shielded_outputs().is_empty()));
 
         // TODO: Separate into checking proofs, signatures, and other structural details.
-        let mut ctx = SaplingVerificationContext::new(true);
+        let mut ctx = SaplingVerificationContext::new();
 
         if !bundle.shielded_spends().is_empty() {
             eprintln!(" - {} Sapling Spend(s)", bundle.shielded_spends().len());
@@ -386,7 +385,7 @@ pub(crate) fn inspect(tx: Transaction, context: Option<Context>) {
                         spend.cv(),
                         *spend.anchor(),
                         &spend.nullifier().0,
-                        spend.rk().clone(),
+                        *spend.rk(),
                         sighash.as_ref(),
                         *spend.spend_auth_sig(),
                         groth16::Proof::read(&spend.zkproof()[..]).unwrap(),
@@ -411,8 +410,11 @@ pub(crate) fn inspect(tx: Transaction, context: Option<Context>) {
                         .and_then(|ctx| ctx.network().zip(ctx.addr_network()))
                     {
                         if let Some((note, addr, memo)) = try_output_recovery_with_ovk(
-                            &SaplingDomain::for_height(params, height.unwrap()),
-                            &zcash_primitives::keys::OutgoingViewingKey([0; 32]),
+                            &SaplingDomain::new(sapling_zip212_enforcement(
+                                &params,
+                                height.unwrap(),
+                            )),
+                            &sapling::keys::OutgoingViewingKey([0; 32]),
                             output,
                             output.cv(),
                             output.out_ciphertext(),
@@ -426,6 +428,7 @@ pub(crate) fn inspect(tx: Transaction, context: Option<Context>) {
                                 eprintln!("     - {}", zaddr);
                                 eprintln!("     - {}", render_value(note.value().inner()));
                             }
+                            let memo = MemoBytes::from_bytes(&memo).expect("correct length");
                             eprintln!("     - {}", render_memo(memo));
                         } else {
                             eprintln!(
diff --git a/src/rust/include/rust/builder.h b/src/rust/include/rust/builder.h
index 8e16782151f..716841452bd 100644
--- a/src/rust/include/rust/builder.h
+++ b/src/rust/include/rust/builder.h
@@ -35,8 +35,7 @@ void orchard_spend_info_free(OrchardSpendInfoPtr* ptr);
 ///
 /// If `anchor` is `null`, the root of the empty Orchard commitment tree is used.
 OrchardBuilderPtr* orchard_builder_new(
-    bool spends_enabled,
-    bool outputs_enabled,
+    bool coinbase,
     const unsigned char* anchor);
 
 /// Frees an Orchard builder returned from `orchard_builder_new`.
diff --git a/src/rust/src/address_ffi.rs b/src/rust/src/address_ffi.rs
index c05da6f87d6..a7adda677b3 100644
--- a/src/rust/src/address_ffi.rs
+++ b/src/rust/src/address_ffi.rs
@@ -9,7 +9,6 @@ use zcash_address::{
     unified::{self, Container, Encoding},
     Network, ToAddress, TryFromAddress, ZcashAddress,
 };
-use zcash_primitives::sapling;
 
 pub type UnifiedAddressObj = NonNull<c_void>;
 pub type AddOrchardReceiverCb =
diff --git a/src/rust/src/bridge.rs b/src/rust/src/bridge.rs
index 6cf15095fe6..268e90501f5 100644
--- a/src/rust/src/bridge.rs
+++ b/src/rust/src/bridge.rs
@@ -185,11 +185,15 @@ pub(crate) mod ffi {
         type SaplingBuilder;
 
         #[cxx_name = "new_builder"]
-        fn new_sapling_builder(network: &Network, height: u32) -> Box<SaplingBuilder>;
+        fn new_sapling_builder(
+            network: &Network,
+            height: u32,
+            anchor: [u8; 32],
+            coinbase: bool,
+        ) -> Result<Box<SaplingBuilder>>;
         fn add_spend(
             self: &mut SaplingBuilder,
             extsk: &[u8],
-            diversifier: [u8; 11],
             recipient: [u8; 43],
             value: u64,
             rcm: [u8; 32],
@@ -205,7 +209,6 @@ pub(crate) mod ffi {
         #[cxx_name = "build_bundle"]
         fn build_sapling_bundle(
             builder: Box<SaplingBuilder>,
-            target_height: u32,
         ) -> Result<Box<SaplingUnauthorizedBundle>>;
 
         #[cxx_name = "UnauthorizedBundle"]
diff --git a/src/rust/src/builder_ffi.rs b/src/rust/src/builder_ffi.rs
index cd9b72c7df9..10d9b1fc147 100644
--- a/src/rust/src/builder_ffi.rs
+++ b/src/rust/src/builder_ffi.rs
@@ -6,8 +6,8 @@ use incrementalmerkletree::Hashable;
 use libc::size_t;
 use orchard::keys::SpendingKey;
 use orchard::{
-    builder::{Builder, InProgress, Unauthorized, Unproven},
-    bundle::{Authorized, Flags},
+    builder::{Builder, BundleType, InProgress, Unauthorized, Unproven},
+    bundle::Authorized,
     keys::{FullViewingKey, OutgoingViewingKey},
     tree::{MerkleHashOrchard, MerklePath},
     value::NoteValue,
@@ -18,7 +18,7 @@ use tracing::error;
 use zcash_primitives::{
     consensus::BranchId,
     transaction::{
-        components::{sapling, Amount},
+        components::Amount,
         sighash::{signature_hash, SignableInput},
         txid::TxIdDigester,
         Authorization, Transaction, TransactionData,
@@ -55,18 +55,16 @@ pub extern "C" fn orchard_spend_info_free(spend_info: *mut OrchardSpendInfo) {
 }
 
 #[no_mangle]
-pub extern "C" fn orchard_builder_new(
-    spends_enabled: bool,
-    outputs_enabled: bool,
-    anchor: *const [u8; 32],
-) -> *mut Builder {
+pub extern "C" fn orchard_builder_new(coinbase: bool, anchor: *const [u8; 32]) -> *mut Builder {
+    let bundle_type = if coinbase {
+        BundleType::Coinbase
+    } else {
+        BundleType::DEFAULT
+    };
     let anchor = unsafe { anchor.as_ref() }
         .map(|a| orchard::Anchor::from_bytes(*a).unwrap())
         .unwrap_or_else(|| MerkleHashOrchard::empty_root(32.into()).into());
-    Box::into_raw(Box::new(Builder::new(
-        Flags::from_parts(spends_enabled, outputs_enabled),
-        anchor,
-    )))
+    Box::into_raw(Box::new(Builder::new(bundle_type, anchor)))
 }
 
 #[no_mangle]
@@ -106,7 +104,7 @@ pub extern "C" fn orchard_builder_add_recipient(
     let value = NoteValue::from_raw(value);
     let memo = unsafe { memo.as_ref() }.copied();
 
-    match builder.add_recipient(ovk, *recipient, value, memo) {
+    match builder.add_output(ovk, *recipient, value, memo) {
         Ok(()) => true,
         Err(e) => {
             error!("Failed to add Orchard recipient: {}", e);
@@ -132,8 +130,15 @@ pub extern "C" fn orchard_builder_build(
     }
     let builder = unsafe { Box::from_raw(builder) };
 
-    match builder.build(OsRng) {
-        Ok(bundle) => Box::into_raw(Box::new(bundle)),
+    match builder.build::<Amount>(OsRng) {
+        Ok(Some((bundle, _))) => Box::into_raw(Box::new(bundle)),
+        Ok(None) => {
+            // The C++ side only calls `orchard_builder_build` when it expects the
+            // resulting bundle to be non-empty (either at least one Orchard output for
+            // coinbase transactions, or a potentially-empty bundle that gets padded).
+            error!("Tried to build empty Orchard bundle");
+            ptr::null_mut()
+        }
         Err(e) => {
             error!("Failed to build Orchard bundle: {:?}", e);
             ptr::null_mut()
@@ -232,7 +237,8 @@ pub(crate) fn shielded_signature_digest(
     struct Signable {}
     impl Authorization for Signable {
         type TransparentAuth = TransparentAuth;
-        type SaplingAuth = sapling::builder::Unauthorized;
+        type SaplingAuth =
+            sapling::builder::InProgress<sapling::builder::Proven, sapling::builder::Unsigned>;
         type OrchardAuth = InProgress<Unproven, Unauthorized>;
     }
 
diff --git a/src/rust/src/incremental_merkle_tree.rs b/src/rust/src/incremental_merkle_tree.rs
index df22496d128..10d2205b8e0 100644
--- a/src/rust/src/incremental_merkle_tree.rs
+++ b/src/rust/src/incremental_merkle_tree.rs
@@ -5,12 +5,9 @@ use std::io::{self, Read, Write};
 use bridgetree::{BridgeTree, Checkpoint, MerkleBridge};
 use incrementalmerkletree::{Address, Hashable, Level, Position};
 use zcash_encoding::{Optional, Vector};
-use zcash_primitives::{
-    merkle_tree::{
-        read_address, read_leu64_usize, read_nonempty_frontier_v1, read_position, write_address,
-        write_nonempty_frontier_v1, write_position, write_usize_leu64, HashSer,
-    },
-    sapling::NOTE_COMMITMENT_TREE_DEPTH,
+use zcash_primitives::merkle_tree::{
+    read_address, read_leu64_usize, read_nonempty_frontier_v1, read_position, write_address,
+    write_nonempty_frontier_v1, write_position, write_usize_leu64, HashSer,
 };
 
 pub const SER_V1: u8 = 1;
@@ -252,9 +249,9 @@ pub fn write_checkpoint_v3<W: Write>(
 /// ids should always be treated as opaque, totally ordered identifiers without additional
 /// semantics.
 #[allow(clippy::redundant_closure)]
-pub fn read_tree<H: Hashable + HashSer + Ord + Clone, R: Read>(
+pub fn read_tree<H: Hashable + HashSer + Ord + Clone, const DEPTH: u8, R: Read>(
     mut reader: R,
-) -> io::Result<BridgeTree<H, u32, NOTE_COMMITMENT_TREE_DEPTH>> {
+) -> io::Result<BridgeTree<H, u32, DEPTH>> {
     let tree_version = reader.read_u8()?;
     let prior_bridges = Vector::read(&mut reader, |r| read_bridge(r, tree_version))?;
     let current_bridge = Optional::read(&mut reader, |r| read_bridge(r, tree_version))?;
@@ -309,9 +306,9 @@ pub fn read_tree<H: Hashable + HashSer + Ord + Clone, R: Read>(
     })
 }
 
-pub fn write_tree<H: Hashable + HashSer + Ord, W: Write>(
+pub fn write_tree<H: Hashable + HashSer + Ord, const DEPTH: u8, W: Write>(
     mut writer: W,
-    tree: &BridgeTree<H, u32, NOTE_COMMITMENT_TREE_DEPTH>,
+    tree: &BridgeTree<H, u32, DEPTH>,
 ) -> io::Result<()> {
     writer.write_u8(SER_V3)?;
     Vector::write(&mut writer, tree.prior_bridges(), |w, b| write_bridge(w, b))?;
diff --git a/src/rust/src/init.rs b/src/rust/src/init.rs
index 67de7e97737..f7a800daf01 100644
--- a/src/rust/src/init.rs
+++ b/src/rust/src/init.rs
@@ -2,8 +2,8 @@ use std::ffi::OsString;
 use std::path::PathBuf;
 use std::sync::Once;
 
-use bellman::groth16::Parameters;
 use bls12_381::Bls12;
+use sapling::circuit::{OutputParameters, SpendParameters};
 use tracing::info;
 
 use crate::{
@@ -51,17 +51,18 @@ fn zksnark_params(sprout_path: String, load_proving_keys: bool) {
         // Load params
         let (sapling_spend_params, sapling_output_params) = {
             let (spend_buf, output_buf) = wagyu_zcash_parameters::load_sapling_parameters();
-            let spend_params = Parameters::<Bls12>::read(&spend_buf[..], false)
-                .expect("couldn't deserialize Sapling spend parameters");
-            let output_params = Parameters::<Bls12>::read(&output_buf[..], false)
-                .expect("couldn't deserialize Sapling spend parameters");
-            (spend_params, output_params)
+            (
+                SpendParameters::read(&spend_buf[..], false)
+                    .expect("Failed to read Sapling Spend parameters"),
+                OutputParameters::read(&output_buf[..], false)
+                    .expect("Failed to read Sapling Output parameters"),
+            )
         };
 
         // We need to clone these because we aren't necessarily storing the proving
         // parameters in memory.
-        let sapling_spend_vk = sapling_spend_params.vk.clone();
-        let sapling_output_vk = sapling_output_params.vk.clone();
+        let sapling_spend_vk = sapling_spend_params.verifying_key();
+        let sapling_output_vk = sapling_output_params.verifying_key();
 
         // Generate Orchard parameters.
         info!(target: "main", "Loading Orchard parameters");
diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs
index 3c77c264378..b9d9b103baf 100644
--- a/src/rust/src/lib.rs
+++ b/src/rust/src/lib.rs
@@ -19,7 +19,10 @@
 // See https://github.com/rust-lang/rfcs/pull/2585 for more background.
 #![allow(clippy::not_unsafe_ptr_arg_deref)]
 
-use bellman::groth16::{self, Parameters, PreparedVerifyingKey};
+use ::sapling::circuit::{
+    OutputParameters, OutputVerifyingKey, SpendParameters, SpendVerifyingKey,
+};
+use bellman::groth16::PreparedVerifyingKey;
 use bls12_381::Bls12;
 use std::path::PathBuf;
 use subtle::CtOption;
@@ -61,12 +64,12 @@ mod test_harness_ffi;
 #[cfg(test)]
 mod tests;
 
-static mut SAPLING_SPEND_VK: Option<groth16::VerifyingKey<Bls12>> = None;
-static mut SAPLING_OUTPUT_VK: Option<groth16::VerifyingKey<Bls12>> = None;
+static mut SAPLING_SPEND_VK: Option<SpendVerifyingKey> = None;
+static mut SAPLING_OUTPUT_VK: Option<OutputVerifyingKey> = None;
 static mut SPROUT_GROTH16_VK: Option<PreparedVerifyingKey<Bls12>> = None;
 
-static mut SAPLING_SPEND_PARAMS: Option<Parameters<Bls12>> = None;
-static mut SAPLING_OUTPUT_PARAMS: Option<Parameters<Bls12>> = None;
+static mut SAPLING_SPEND_PARAMS: Option<SpendParameters> = None;
+static mut SAPLING_OUTPUT_PARAMS: Option<OutputParameters> = None;
 static mut SPROUT_GROTH16_PARAMS_PATH: Option<PathBuf> = None;
 
 static mut ORCHARD_PK: Option<orchard::circuit::ProvingKey> = None;
diff --git a/src/rust/src/merkle_frontier.rs b/src/rust/src/merkle_frontier.rs
index ae4b3b7d71a..d53a08213b6 100644
--- a/src/rust/src/merkle_frontier.rs
+++ b/src/rust/src/merkle_frontier.rs
@@ -5,9 +5,9 @@ use incrementalmerkletree::{
     Hashable, Level,
 };
 use orchard::tree::MerkleHashOrchard;
-use zcash_primitives::{
-    merkle_tree::{read_frontier_v1, write_commitment_tree, write_frontier_v1, HashSer},
-    sapling::NOTE_COMMITMENT_TREE_DEPTH,
+use sapling::NOTE_COMMITMENT_TREE_DEPTH;
+use zcash_primitives::merkle_tree::{
+    read_frontier_v1, write_commitment_tree, write_frontier_v1, HashSer,
 };
 
 use crate::{bridge::ffi, orchard_bundle, streams::CppStream, wallet::Wallet};
diff --git a/src/rust/src/note_encryption.rs b/src/rust/src/note_encryption.rs
index 069fc712c27..5806bf68fc0 100644
--- a/src/rust/src/note_encryption.rs
+++ b/src/rust/src/note_encryption.rs
@@ -1,19 +1,15 @@
 use std::convert::TryInto;
 
+use sapling::{
+    keys::OutgoingViewingKey,
+    note_encryption::{PreparedIncomingViewingKey, SaplingDomain},
+    value::ValueCommitment,
+    SaplingIvk,
+};
 use zcash_note_encryption::{
     try_output_recovery_with_ovk, Domain, EphemeralKeyBytes, ShieldedOutput, ENC_CIPHERTEXT_SIZE,
 };
-use zcash_primitives::{
-    consensus::BlockHeight,
-    keys::OutgoingViewingKey,
-    memo::MemoBytes,
-    sapling::{
-        self,
-        note_encryption::{PreparedIncomingViewingKey, SaplingDomain},
-        value::ValueCommitment,
-        SaplingIvk,
-    },
-};
+use zcash_primitives::consensus::{sapling_zip212_enforcement, BlockHeight};
 
 use crate::{bridge::ffi::SaplingShieldedOutput, params::Network};
 
@@ -35,10 +31,9 @@ pub(crate) fn try_sapling_note_decryption(
         .ok_or("Invalid Sapling ivk passed to wallet::try_sapling_note_decryption()")?;
 
     let (note, recipient, memo) = sapling::note_encryption::try_sapling_note_decryption(
-        network,
-        BlockHeight::from_u32(height),
         &ivk,
         &output,
+        sapling_zip212_enforcement(network, BlockHeight::from_u32(height)),
     )
     .ok_or("Decryption failed")?;
 
@@ -64,7 +59,10 @@ pub(crate) fn try_sapling_output_recovery(
     ovk: [u8; 32],
     output: SaplingShieldedOutput,
 ) -> Result<Box<DecryptedSaplingOutput>, &'static str> {
-    let domain = SaplingDomain::for_height(*network, BlockHeight::from_u32(height));
+    let domain = SaplingDomain::new(sapling_zip212_enforcement(
+        network,
+        BlockHeight::from_u32(height),
+    ));
 
     let cv = Option::from(ValueCommitment::from_bytes_not_small_order(&output.cv))
         .ok_or("Invalid output.cv passed to wallet::try_sapling_note_decryption()")?;
@@ -94,12 +92,12 @@ pub(crate) fn parse_and_prepare_sapling_ivk(
         .into()
 }
 
-impl ShieldedOutput<SaplingDomain<Network>, ENC_CIPHERTEXT_SIZE> for SaplingShieldedOutput {
+impl ShieldedOutput<SaplingDomain, ENC_CIPHERTEXT_SIZE> for SaplingShieldedOutput {
     fn ephemeral_key(&self) -> EphemeralKeyBytes {
         EphemeralKeyBytes(self.ephemeral_key)
     }
 
-    fn cmstar_bytes(&self) -> <SaplingDomain<Network> as Domain>::ExtractedCommitmentBytes {
+    fn cmstar_bytes(&self) -> <SaplingDomain as Domain>::ExtractedCommitmentBytes {
         self.cmu
     }
 
@@ -112,7 +110,7 @@ impl ShieldedOutput<SaplingDomain<Network>, ENC_CIPHERTEXT_SIZE> for SaplingShie
 pub(crate) struct DecryptedSaplingOutput {
     note: sapling::Note,
     recipient: sapling::PaymentAddress,
-    memo: MemoBytes,
+    memo: [u8; 512],
 }
 
 impl DecryptedSaplingOutput {
@@ -140,6 +138,6 @@ impl DecryptedSaplingOutput {
     }
 
     pub(crate) fn memo(&self) -> [u8; 512] {
-        *self.memo.as_array()
+        self.memo
     }
 }
diff --git a/src/rust/src/orchard_keys_ffi.rs b/src/rust/src/orchard_keys_ffi.rs
index f992a3759d5..87a551ba30f 100644
--- a/src/rust/src/orchard_keys_ffi.rs
+++ b/src/rust/src/orchard_keys_ffi.rs
@@ -1,3 +1,4 @@
+use std::convert::TryInto;
 use std::io::{Read, Write};
 use std::slice;
 use tracing::error;
@@ -160,7 +161,7 @@ pub extern "C" fn orchard_incoming_viewing_key_decrypt_diversifier(
 
     match key.diversifier_index(addr) {
         Some(j) => {
-            j_ret.copy_from_slice(j.to_bytes());
+            j_ret.copy_from_slice(j.as_bytes());
             true
         }
         None => false,
@@ -327,7 +328,8 @@ pub extern "C" fn orchard_spending_key_for_account(
     account_id: u32,
 ) -> *mut SpendingKey {
     let seed = unsafe { slice::from_raw_parts(seed, seed_len) };
-    SpendingKey::from_zip32_seed(seed, bip44_coin_type, account_id)
+    let account = account_id.try_into().expect("account_id should be a u31");
+    SpendingKey::from_zip32_seed(seed, bip44_coin_type, account)
         .map(|key| Box::into_raw(Box::new(key)))
         .unwrap_or(std::ptr::null_mut())
 }
diff --git a/src/rust/src/sapling.rs b/src/rust/src/sapling.rs
index 895d7ab1494..487348487fb 100644
--- a/src/rust/src/sapling.rs
+++ b/src/rust/src/sapling.rs
@@ -2,37 +2,35 @@
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or https://www.opensource.org/licenses/mit-license.php .
 
-use std::convert::TryInto;
-use std::io::{self, Read, Write};
+use std::convert::{TryFrom, TryInto};
+use std::io;
 use std::mem;
 
-use bellman::groth16::{prepare_verifying_key, Proof};
+use bellman::groth16::Proof;
+use bls12_381::Bls12;
 use group::GroupEncoding;
-use incrementalmerkletree::MerklePath;
 use memuse::DynamicUsage;
-use rand_core::OsRng;
-use zcash_encoding::Vector;
+use rand_core::{OsRng, RngCore};
+use sapling::{
+    builder::BundleType,
+    circuit::{self, OutputParameters, SpendParameters},
+    keys::{OutgoingViewingKey, SpendAuthorizingKey},
+    note::ExtractedNoteCommitment,
+    prover::{OutputProver, SpendProver},
+    value::{NoteValue, ValueCommitTrapdoor, ValueCommitment},
+    zip32::ExtendedSpendingKey,
+    Anchor, Diversifier, MerklePath, Note, PaymentAddress, ProofGenerationKey, Rseed,
+    SaplingVerificationContext,
+};
 use zcash_primitives::{
-    keys::OutgoingViewingKey,
+    consensus::sapling_zip212_enforcement,
     memo::MemoBytes,
     merkle_tree::merkle_path_from_slice,
-    sapling::{
-        note::ExtractedNoteCommitment,
-        prover::TxProver,
-        redjubjub::{self, Signature},
-        value::{NoteValue, ValueCommitment},
-        Diversifier, Node, Note, PaymentAddress, ProofGenerationKey, Rseed,
-        NOTE_COMMITMENT_TREE_DEPTH,
-    },
     transaction::{
-        components::{sapling, Amount},
+        components::{sapling as sapling_serialization, Amount},
         txid::{BlockTxCommitmentDigester, TxIdDigester},
         Authorized, Transaction, TransactionDigest,
     },
-    zip32::ExtendedSpendingKey,
-};
-use zcash_proofs::sapling::{
-    self as sapling_proofs, SaplingProvingContext, SaplingVerificationContext,
 };
 
 use super::GROTH_PROOF_SIZE;
@@ -50,10 +48,10 @@ mod zip32;
 
 const SAPLING_TREE_DEPTH: usize = 32;
 
-pub(crate) struct Spend(sapling::SpendDescription<sapling::Authorized>);
+pub(crate) struct Spend(sapling::bundle::SpendDescription<sapling::bundle::Authorized>);
 
 pub(crate) fn parse_v4_sapling_spend(bytes: &[u8]) -> Result<Box<Spend>, String> {
-    sapling::SpendDescription::read(&mut io::Cursor::new(bytes))
+    sapling_serialization::temporary_zcashd_read_spend_v4(&mut io::Cursor::new(bytes))
         .map(Spend)
         .map(Box::new)
         .map_err(|e| format!("{}", e))
@@ -73,7 +71,7 @@ impl Spend {
     }
 
     pub(crate) fn rk(&self) -> [u8; 32] {
-        self.0.rk().0.to_bytes()
+        (*self.0.rk()).into()
     }
 
     pub(crate) fn zkproof(&self) -> [u8; 192] {
@@ -81,19 +79,14 @@ impl Spend {
     }
 
     pub(crate) fn spend_auth_sig(&self) -> [u8; 64] {
-        let mut ret = [0; 64];
-        self.0
-            .spend_auth_sig()
-            .write(&mut ret[..])
-            .expect("correct length");
-        ret
+        (*self.0.spend_auth_sig()).into()
     }
 }
 
-pub(crate) struct Output(sapling::OutputDescription<[u8; 192]>);
+pub(crate) struct Output(sapling::bundle::OutputDescription<[u8; 192]>);
 
 pub(crate) fn parse_v4_sapling_output(bytes: &[u8]) -> Result<Box<Output>, String> {
-    sapling::OutputDescription::read(&mut io::Cursor::new(bytes))
+    sapling_serialization::temporary_zcashd_read_output_v4(&mut io::Cursor::new(bytes))
         .map(Output)
         .map(Box::new)
         .map_err(|e| format!("{}", e))
@@ -125,14 +118,13 @@ impl Output {
     }
 
     pub(crate) fn serialize_v4(&self, writer: &mut CppStream<'_>) -> Result<(), String> {
-        self.0
-            .write_v4(writer)
+        sapling_serialization::temporary_zcashd_write_output_v4(writer, &self.0)
             .map_err(|e| format!("Failed to write v4 Sapling Output Description: {}", e))
     }
 }
 
 #[derive(Clone)]
-pub(crate) struct Bundle(pub(crate) Option<sapling::Bundle<sapling::Authorized>>);
+pub(crate) struct Bundle(pub(crate) Option<sapling::Bundle<sapling::bundle::Authorized, Amount>>);
 
 pub(crate) fn none_sapling_bundle() -> Box<Bundle> {
     Box::new(Bundle(None))
@@ -159,37 +151,15 @@ impl Bundle {
 
     pub(crate) fn serialize_v4_components(
         &self,
-        mut writer: &mut CppStream<'_>,
+        writer: &mut CppStream<'_>,
         has_sapling: bool,
     ) -> Result<(), String> {
-        if has_sapling {
-            let mut write_v4 = || {
-                writer.write_all(
-                    &self
-                        .0
-                        .as_ref()
-                        .map_or(Amount::zero(), |b| *b.value_balance())
-                        .to_i64_le_bytes(),
-                )?;
-                Vector::write(
-                    &mut writer,
-                    self.0.as_ref().map_or(&[], |b| b.shielded_spends()),
-                    |w, e| e.write_v4(w),
-                )?;
-                Vector::write(
-                    &mut writer,
-                    self.0.as_ref().map_or(&[], |b| b.shielded_outputs()),
-                    |w, e| e.write_v4(w),
-                )
-            };
-            write_v4().map_err(|e| format!("{}", e))?;
-        } else if self.0.is_some() {
-            return Err(
-                "Sapling components may not be present if Sapling is not active.".to_string(),
-            );
-        }
-
-        Ok(())
+        sapling_serialization::temporary_zcashd_write_v4_components(
+            writer,
+            self.0.as_ref(),
+            has_sapling,
+        )
+        .map_err(|e| format!("{}", e))
     }
 
     /// Serializes an authorized Sapling bundle to the given stream.
@@ -200,7 +170,7 @@ impl Bundle {
             .map_err(|e| format!("Failed to serialize Sapling bundle: {}", e))
     }
 
-    pub(crate) fn inner(&self) -> Option<&sapling::Bundle<sapling::Authorized>> {
+    pub(crate) fn inner(&self) -> Option<&sapling::Bundle<sapling::bundle::Authorized, Amount>> {
         self.0.as_ref()
     }
 
@@ -264,14 +234,11 @@ impl Bundle {
     ///
     /// Panics if the bundle is not present.
     pub(crate) fn binding_sig(&self) -> [u8; 64] {
-        let mut ret = [0; 64];
         self.inner()
             .expect("Bundle actions should have been checked to be non-empty")
             .authorization()
             .binding_sig
-            .write(&mut ret[..])
-            .expect("correct length");
-        ret
+            .into()
     }
 
     fn commitment<D: TransactionDigest<Authorized>>(&self, digester: D) -> D::SaplingDigest {
@@ -281,8 +248,8 @@ impl Bundle {
 
 pub(crate) struct BundleAssembler {
     value_balance: Amount,
-    shielded_spends: Vec<sapling::SpendDescription<sapling::Authorized>>,
-    shielded_outputs: Vec<sapling::OutputDescription<[u8; 192]>>, // GROTH_PROOF_SIZE
+    shielded_spends: Vec<sapling::bundle::SpendDescription<sapling::bundle::Authorized>>,
+    shielded_outputs: Vec<sapling::bundle::OutputDescription<[u8; 192]>>, // GROTH_PROOF_SIZE
 }
 
 pub(crate) fn new_bundle_assembler() -> Box<BundleAssembler> {
@@ -303,27 +270,11 @@ pub(crate) fn parse_v4_sapling_components(
 
 impl BundleAssembler {
     pub(crate) fn parse_v4_components(
-        mut reader: &mut CppStream<'_>,
+        reader: &mut CppStream<'_>,
         has_sapling: bool,
     ) -> io::Result<Box<Self>> {
-        let (value_balance, shielded_spends, shielded_outputs) = if has_sapling {
-            let vb = {
-                let mut tmp = [0; 8];
-                reader.read_exact(&mut tmp)?;
-                Amount::from_i64_le_bytes(tmp).map_err(|_| {
-                    io::Error::new(io::ErrorKind::InvalidData, "valueBalance out of range")
-                })?
-            };
-            #[allow(clippy::redundant_closure)]
-            let ss: Vec<sapling::SpendDescription<sapling::Authorized>> =
-                Vector::read(&mut reader, |r| sapling::SpendDescription::read(r))?;
-            #[allow(clippy::redundant_closure)]
-            let so: Vec<sapling::OutputDescription<sapling::GrothProofBytes>> =
-                Vector::read(&mut reader, |r| sapling::OutputDescription::read(r))?;
-            (vb, ss, so)
-        } else {
-            (Amount::zero(), vec![], vec![])
-        };
+        let (value_balance, shielded_spends, shielded_outputs) =
+            sapling_serialization::temporary_zcashd_read_v4_components(reader, has_sapling)?;
 
         Ok(Box::new(Self {
             value_balance,
@@ -342,125 +293,119 @@ pub(crate) fn finish_bundle_assembly(
     assembler: Box<BundleAssembler>,
     binding_sig: [u8; 64],
 ) -> Box<Bundle> {
-    let binding_sig = redjubjub::Signature::read(&binding_sig[..]).expect("parsed elsewhere");
+    let binding_sig = redjubjub::Signature::from(binding_sig);
 
-    Box::new(Bundle(Some(sapling::Bundle::temporary_zcashd_from_parts(
+    Box::new(Bundle(sapling::Bundle::from_parts(
         assembler.shielded_spends,
         assembler.shielded_outputs,
         assembler.value_balance,
-        sapling::Authorized { binding_sig },
-    ))))
+        sapling::bundle::Authorized { binding_sig },
+    )))
 }
 
 pub(crate) struct StaticTxProver;
 
-impl TxProver for StaticTxProver {
-    type SaplingProvingContext = SaplingProvingContext;
-
-    fn new_sapling_proving_context(&self) -> Self::SaplingProvingContext {
-        SaplingProvingContext::new()
-    }
+impl SpendProver for StaticTxProver {
+    type Proof = Proof<Bls12>;
 
-    fn spend_proof(
-        &self,
-        ctx: &mut Self::SaplingProvingContext,
+    fn prepare_circuit(
         proof_generation_key: ProofGenerationKey,
         diversifier: Diversifier,
         rseed: Rseed,
-        ar: jubjub::Fr,
-        value: u64,
+        value: NoteValue,
+        alpha: jubjub::Fr,
+        rcv: ValueCommitTrapdoor,
         anchor: bls12_381::Scalar,
-        merkle_path: MerklePath<Node, NOTE_COMMITMENT_TREE_DEPTH>,
-    ) -> Result<
-        (
-            [u8; GROTH_PROOF_SIZE],
-            ValueCommitment,
-            redjubjub::PublicKey,
-        ),
-        (),
-    > {
-        let (proof, cv, rk) = ctx.spend_proof(
+        merkle_path: MerklePath,
+    ) -> Option<circuit::Spend> {
+        SpendParameters::prepare_circuit(
             proof_generation_key,
             diversifier,
             rseed,
-            ar,
             value,
+            alpha,
+            rcv,
             anchor,
             merkle_path,
-            unsafe { SAPLING_SPEND_PARAMS.as_ref() }
-                .expect("Parameters not loaded: SAPLING_SPEND_PARAMS should have been initialized"),
-            &prepare_verifying_key(
-                unsafe { SAPLING_SPEND_VK.as_ref() }
-                    .expect("Parameters not loaded: SAPLING_SPEND_VK should have been initialized"),
-            ),
-        )?;
+        )
+    }
 
-        let mut zkproof = [0u8; GROTH_PROOF_SIZE];
-        proof
-            .write(&mut zkproof[..])
-            .expect("should be able to serialize a proof");
+    fn create_proof<R: RngCore>(&self, circuit: circuit::Spend, rng: &mut R) -> Self::Proof {
+        unsafe { SAPLING_SPEND_PARAMS.as_ref() }
+            .expect("Parameters not loaded: SAPLING_SPEND_PARAMS should have been initialized")
+            .create_proof(circuit, rng)
+    }
 
-        Ok((zkproof, cv, rk))
+    fn encode_proof(proof: Self::Proof) -> sapling::bundle::GrothProofBytes {
+        SpendParameters::encode_proof(proof)
     }
+}
 
-    fn output_proof(
-        &self,
-        ctx: &mut Self::SaplingProvingContext,
+impl OutputProver for StaticTxProver {
+    type Proof = Proof<Bls12>;
+
+    fn prepare_circuit(
         esk: jubjub::Fr,
         payment_address: PaymentAddress,
         rcm: jubjub::Fr,
-        value: u64,
-    ) -> ([u8; GROTH_PROOF_SIZE], ValueCommitment) {
-        let (proof, cv) = ctx.output_proof(
-            esk,
-            payment_address,
-            rcm,
-            value,
-            unsafe { SAPLING_OUTPUT_PARAMS.as_ref() }.expect(
-                "Parameters not loaded: SAPLING_OUTPUT_PARAMS should have been initialized",
-            ),
-        );
-
-        let mut zkproof = [0u8; GROTH_PROOF_SIZE];
-        proof
-            .write(&mut zkproof[..])
-            .expect("should be able to serialize a proof");
+        value: NoteValue,
+        rcv: ValueCommitTrapdoor,
+    ) -> circuit::Output {
+        OutputParameters::prepare_circuit(esk, payment_address, rcm, value, rcv)
+    }
 
-        (zkproof, cv)
+    fn create_proof<R: RngCore>(&self, circuit: circuit::Output, rng: &mut R) -> Self::Proof {
+        unsafe { SAPLING_OUTPUT_PARAMS.as_ref() }
+            .expect("Parameters not loaded: SAPLING_OUTPUT_PARAMS should have been initialized")
+            .create_proof(circuit, rng)
     }
 
-    fn binding_sig(
-        &self,
-        ctx: &mut Self::SaplingProvingContext,
-        value_balance: zcash_primitives::transaction::components::Amount,
-        sighash: &[u8; 32],
-    ) -> Result<redjubjub::Signature, ()> {
-        ctx.binding_sig(value_balance, sighash)
+    fn encode_proof(proof: Self::Proof) -> sapling::bundle::GrothProofBytes {
+        OutputParameters::encode_proof(proof)
     }
 }
 
-pub(crate) struct SaplingBuilder(sapling::builder::SaplingBuilder<Network>);
+pub(crate) struct SaplingBuilder {
+    builder: sapling::builder::Builder,
+    signing_keys: Vec<SpendAuthorizingKey>,
+}
 
-pub(crate) fn new_sapling_builder(network: &Network, target_height: u32) -> Box<SaplingBuilder> {
-    Box::new(SaplingBuilder(sapling::builder::SaplingBuilder::new(
-        *network,
-        target_height.into(),
-    )))
+pub(crate) fn new_sapling_builder(
+    network: &Network,
+    target_height: u32,
+    anchor: [u8; 32],
+    coinbase: bool,
+) -> Result<Box<SaplingBuilder>, String> {
+    let bundle_type = if coinbase {
+        BundleType::Coinbase
+    } else {
+        BundleType::DEFAULT
+    };
+
+    let anchor = Option::from(Anchor::from_bytes(anchor))
+        .ok_or_else(|| "Invalid Sapling anchor".to_owned())?;
+
+    Ok(Box::new(SaplingBuilder {
+        builder: sapling::builder::Builder::new(
+            sapling_zip212_enforcement(network, target_height.into()),
+            bundle_type,
+            anchor,
+        ),
+        signing_keys: vec![],
+    }))
 }
 
 #[allow(clippy::boxed_local)]
 pub(crate) fn build_sapling_bundle(
     builder: Box<SaplingBuilder>,
-    target_height: u32,
 ) -> Result<Box<SaplingUnauthorizedBundle>, String> {
-    builder.build(target_height).map(Box::new)
+    builder.build().map(Box::new)
 }
 
 impl SaplingBuilder {
     pub(crate) fn add_spend(
         &mut self,
         extsk: &[u8],
-        diversifier: [u8; 11],
         recipient: [u8; 43],
         value: u64,
         rcm: [u8; 32],
@@ -468,7 +413,6 @@ impl SaplingBuilder {
     ) -> Result<(), String> {
         let extsk =
             ExtendedSpendingKey::from_bytes(extsk).map_err(|_| "Invalid ExtSK".to_owned())?;
-        let diversifier = Diversifier(diversifier);
         let recipient =
             PaymentAddress::from_bytes(&recipient).ok_or("Invalid recipient address")?;
         let value = NoteValue::from_raw(value);
@@ -481,9 +425,12 @@ impl SaplingBuilder {
         let merkle_path = merkle_path_from_slice(&merkle_path)
             .map_err(|e| format!("Invalid Sapling Merkle path: {}", e))?;
 
-        self.0
-            .add_spend(OsRng, extsk, diversifier, note, merkle_path)
-            .map_err(|e| format!("Failed to add Sapling spend: {}", e))
+        self.builder
+            .add_spend(&extsk, note, merkle_path)
+            .map_err(|e| format!("Failed to add Sapling spend: {}", e))?;
+        self.signing_keys.push(extsk.expsk.ask);
+
+        Ok(())
     }
 
     pub(crate) fn add_recipient(
@@ -496,28 +443,39 @@ impl SaplingBuilder {
         let ovk = Some(OutgoingViewingKey(ovk));
         let to = PaymentAddress::from_bytes(&to).ok_or("Invalid recipient address")?;
         let value = NoteValue::from_raw(value);
-        let memo = MemoBytes::from_bytes(&memo).map_err(|e| format!("Invalid memo: {}", e))?;
+        let _ = MemoBytes::from_bytes(&memo).map_err(|e| format!("Invalid memo: {}", e))?;
 
-        self.0
-            .add_output(OsRng, ovk, to, value, memo)
+        self.builder
+            .add_output(ovk, to, value, Some(memo))
             .map_err(|e| format!("Failed to add Sapling recipient: {}", e))
     }
 
-    fn build(self, target_height: u32) -> Result<SaplingUnauthorizedBundle, String> {
+    fn build(self) -> Result<SaplingUnauthorizedBundle, String> {
+        let Self {
+            builder,
+            signing_keys,
+        } = self;
         let prover = crate::sapling::StaticTxProver;
-        let mut ctx = prover.new_sapling_proving_context();
         let rng = OsRng;
-        let bundle = self
-            .0
-            .build(&prover, &mut ctx, rng, target_height.into(), None)
-            .map_err(|e| format!("Failed to build Sapling bundle: {}", e))?;
-        Ok(SaplingUnauthorizedBundle { bundle, ctx })
+        let bundle = builder
+            .build::<StaticTxProver, StaticTxProver, _, Amount>(rng)
+            .map_err(|e| format!("Failed to build Sapling bundle: {}", e))?
+            .map(|(bundle, _)| bundle.create_proofs(&prover, &prover, rng, ()));
+        Ok(SaplingUnauthorizedBundle {
+            bundle,
+            signing_keys,
+        })
     }
 }
 
 pub(crate) struct SaplingUnauthorizedBundle {
-    pub(crate) bundle: Option<sapling::Bundle<sapling::builder::Unauthorized>>,
-    ctx: SaplingProvingContext,
+    pub(crate) bundle: Option<
+        sapling::Bundle<
+            sapling::builder::InProgress<sapling::builder::Proven, sapling::builder::Unsigned>,
+            Amount,
+        >,
+    >,
+    signing_keys: Vec<SpendAuthorizingKey>,
 }
 
 pub(crate) fn apply_sapling_bundle_signatures(
@@ -529,12 +487,14 @@ pub(crate) fn apply_sapling_bundle_signatures(
 
 impl SaplingUnauthorizedBundle {
     fn apply_signatures(self, sighash_bytes: [u8; 32]) -> Result<crate::sapling::Bundle, String> {
-        let SaplingUnauthorizedBundle { bundle, mut ctx } = self;
+        let SaplingUnauthorizedBundle {
+            bundle,
+            signing_keys,
+        } = self;
 
         let authorized = if let Some(bundle) = bundle {
-            let prover = crate::sapling::StaticTxProver;
-            let (authorized, _) = bundle
-                .apply_signatures(&prover, &mut ctx, &mut OsRng, &sighash_bytes)
+            let authorized = bundle
+                .apply_signatures(OsRng, sighash_bytes, &signing_keys)
                 .map_err(|e| format!("Failed to apply signatures to Sapling bundle: {}", e))?;
             Some(authorized)
         } else {
@@ -548,10 +508,7 @@ impl SaplingUnauthorizedBundle {
 pub(crate) struct Verifier(SaplingVerificationContext);
 
 pub(crate) fn init_verifier() -> Box<Verifier> {
-    // We consider ZIP 216 active all of the time because blocks prior to NU5
-    // activation (on mainnet and testnet) did not contain Sapling transactions
-    // that violated its canonicity rule.
-    Box::new(Verifier(SaplingVerificationContext::new(true)))
+    Box::new(Verifier(SaplingVerificationContext::new()))
 }
 
 impl Verifier {
@@ -580,16 +537,13 @@ impl Verifier {
         };
 
         // Deserialize rk
-        let rk = match redjubjub::PublicKey::read(&rk[..]) {
+        let rk = match redjubjub::VerificationKey::try_from(*rk) {
             Ok(p) => p,
             Err(_) => return false,
         };
 
         // Deserialize the signature
-        let spend_auth_sig = match Signature::read(&spend_auth_sig[..]) {
-            Ok(sig) => sig,
-            Err(_) => return false,
-        };
+        let spend_auth_sig = redjubjub::Signature::from(*spend_auth_sig);
 
         // Deserialize the proof
         let zkproof = match Proof::read(&zkproof[..]) {
@@ -605,10 +559,9 @@ impl Verifier {
             sighash_value,
             spend_auth_sig,
             zkproof,
-            &prepare_verifying_key(
-                unsafe { SAPLING_SPEND_VK.as_ref() }
-                    .expect("Parameters not loaded: SAPLING_SPEND_VK should have been initialized"),
-            ),
+            &unsafe { SAPLING_SPEND_VK.as_ref() }
+                .expect("Parameters not loaded: SAPLING_SPEND_VK should have been initialized")
+                .prepare(),
         )
     }
 
@@ -648,11 +601,9 @@ impl Verifier {
             cmu,
             epk,
             zkproof,
-            &prepare_verifying_key(
-                unsafe { SAPLING_OUTPUT_VK.as_ref() }.expect(
-                    "Parameters not loaded: SAPLING_OUTPUT_VK should have been initialized",
-                ),
-            ),
+            &unsafe { SAPLING_OUTPUT_VK.as_ref() }
+                .expect("Parameters not loaded: SAPLING_OUTPUT_VK should have been initialized")
+                .prepare(),
         )
     }
 
@@ -668,10 +619,7 @@ impl Verifier {
         };
 
         // Deserialize the signature
-        let binding_sig = match Signature::read(&binding_sig[..]) {
-            Ok(sig) => sig,
-            Err(_) => return false,
-        };
+        let binding_sig = redjubjub::Signature::from(*binding_sig);
 
         self.0
             .final_check(value_balance, sighash_value, binding_sig)
@@ -679,7 +627,7 @@ impl Verifier {
 }
 
 struct BatchValidatorInner {
-    validator: sapling_proofs::BatchValidator,
+    validator: sapling::BatchValidator,
     queued_entries: CacheEntries,
 }
 
@@ -687,7 +635,7 @@ pub(crate) struct BatchValidator(Option<BatchValidatorInner>);
 
 pub(crate) fn init_batch_validator(cache_store: bool) -> Box<BatchValidator> {
     Box::new(BatchValidator(Some(BatchValidatorInner {
-        validator: sapling_proofs::BatchValidator::new(),
+        validator: sapling::BatchValidator::new(),
         queued_entries: CacheEntries::new(cache_store),
     })))
 }
diff --git a/src/rust/src/sapling/spec.rs b/src/rust/src/sapling/spec.rs
index 87aefb8deb2..3ed3a830a8c 100644
--- a/src/rust/src/sapling/spec.rs
+++ b/src/rust/src/sapling/spec.rs
@@ -5,16 +5,14 @@ use group::{cofactor::CofactorGroup, GroupEncoding};
 use incrementalmerkletree::Hashable;
 use rand_core::{OsRng, RngCore};
 
-use zcash_primitives::{
+use sapling::{
     constants::{CRH_IVK_PERSONALIZATION, PROOF_GENERATION_KEY_GENERATOR, SPENDING_KEY_GENERATOR},
-    merkle_tree::HashSer,
-    sapling::{
-        merkle_hash,
-        note::{ExtractedNoteCommitment, NoteCommitment},
-        value::NoteValue,
-        Diversifier, Node, Note, NullifierDerivingKey, PaymentAddress, Rseed,
-    },
+    merkle_hash,
+    note::{ExtractedNoteCommitment, NoteCommitment},
+    value::NoteValue,
+    Diversifier, Node, Note, NullifierDerivingKey, PaymentAddress, Rseed,
 };
+use zcash_primitives::merkle_tree::HashSer;
 
 use crate::de_ct;
 
diff --git a/src/rust/src/sapling/zip32.rs b/src/rust/src/sapling/zip32.rs
index 3bdb1704574..54e3e2c3740 100644
--- a/src/rust/src/sapling/zip32.rs
+++ b/src/rust/src/sapling/zip32.rs
@@ -1,7 +1,9 @@
-use zcash_primitives::{
-    sapling::{keys::FullViewingKey, Diversifier},
-    zip32::{self, sapling_address, sapling_derive_internal_fvk, sapling_find_address},
+use sapling::{
+    keys::FullViewingKey,
+    zip32::{sapling_address, sapling_derive_internal_fvk, sapling_find_address},
+    Diversifier,
 };
+use zcash_primitives::zip32;
 
 #[cxx::bridge]
 mod ffi {
@@ -20,7 +22,6 @@ mod ffi {
         fn xsk_master(seed: &[u8]) -> [u8; 169];
         fn xsk_derive(xsk_parent: &[u8; 169], i: u32) -> [u8; 169];
         fn xsk_derive_internal(xsk_external: &[u8; 169]) -> [u8; 169];
-        fn xfvk_derive(xfvk_parent: &[u8; 169], i: u32) -> Result<[u8; 169]>;
         fn derive_internal_fvk(fvk: &[u8; 96], dk: [u8; 32]) -> FfiFullViewingKey;
         fn address(fvk: &[u8; 96], dk: [u8; 32], j: [u8; 11]) -> Result<[u8; 43]>;
         fn find_address(fvk: &[u8; 96], dk: [u8; 32], j: [u8; 11]) -> Result<FfiPaymentAddress>;
@@ -30,7 +31,7 @@ mod ffi {
 
 /// Derives the master ExtendedSpendingKey from a seed.
 fn xsk_master(seed: &[u8]) -> [u8; 169] {
-    let xsk = zip32::ExtendedSpendingKey::master(seed);
+    let xsk = sapling::zip32::ExtendedSpendingKey::master(seed);
 
     let mut xsk_master = [0; 169];
     xsk.write(&mut xsk_master[..])
@@ -40,9 +41,9 @@ fn xsk_master(seed: &[u8]) -> [u8; 169] {
 
 /// Derive a child ExtendedSpendingKey from a parent.
 fn xsk_derive(xsk_parent: &[u8; 169], i: u32) -> [u8; 169] {
-    let xsk_parent =
-        zip32::ExtendedSpendingKey::read(&xsk_parent[..]).expect("valid ExtendedSpendingKey");
-    let i = zip32::ChildIndex::from_index(i);
+    let xsk_parent = sapling::zip32::ExtendedSpendingKey::read(&xsk_parent[..])
+        .expect("valid ExtendedSpendingKey");
+    let i = zip32::ChildIndex::from_index(i).expect("non-hardened derivation is unsupported");
 
     let xsk = xsk_parent.derive_child(i);
 
@@ -55,8 +56,8 @@ fn xsk_derive(xsk_parent: &[u8; 169], i: u32) -> [u8; 169] {
 /// Derive the Sapling internal spending key from the external extended
 /// spending key
 fn xsk_derive_internal(xsk_external: &[u8; 169]) -> [u8; 169] {
-    let xsk_external =
-        zip32::ExtendedSpendingKey::read(&xsk_external[..]).expect("valid ExtendedSpendingKey");
+    let xsk_external = sapling::zip32::ExtendedSpendingKey::read(&xsk_external[..])
+        .expect("valid ExtendedSpendingKey");
 
     let xsk_internal = xsk_external.derive_internal();
 
@@ -67,26 +68,10 @@ fn xsk_derive_internal(xsk_external: &[u8; 169]) -> [u8; 169] {
     xsk_internal_ret
 }
 
-/// Derive a child ExtendedFullViewingKey from a parent.
-fn xfvk_derive(xfvk_parent: &[u8; 169], i: u32) -> Result<[u8; 169], String> {
-    let xfvk_parent = zip32::ExtendedFullViewingKey::read(&xfvk_parent[..])
-        .expect("valid ExtendedFullViewingKey");
-    let i = zip32::ChildIndex::from_index(i);
-
-    let xfvk = xfvk_parent
-        .derive_child(i)
-        .map_err(|()| "Cannot derive hardened xfvk".to_string())?;
-
-    let mut xfvk_i = [0; 169];
-    xfvk.write(&mut xfvk_i[..])
-        .expect("should be able to serialize an ExtendedFullViewingKey");
-    Ok(xfvk_i)
-}
-
 /// Derive the Sapling internal full viewing key from the corresponding external full viewing key
 fn derive_internal_fvk(fvk: &[u8; 96], dk: [u8; 32]) -> ffi::FfiFullViewingKey {
     let fvk = FullViewingKey::read(&fvk[..]).expect("valid Sapling FullViewingKey");
-    let dk = zip32::sapling::DiversifierKey::from_bytes(dk);
+    let dk = sapling::zip32::DiversifierKey::from_bytes(dk);
 
     let (fvk_internal, dk_internal) = sapling_derive_internal_fvk(&fvk, &dk);
 
@@ -99,8 +84,8 @@ fn derive_internal_fvk(fvk: &[u8; 96], dk: [u8; 32]) -> ffi::FfiFullViewingKey {
 /// Derive a PaymentAddress from an ExtendedFullViewingKey.
 fn address(fvk: &[u8; 96], dk: [u8; 32], j: [u8; 11]) -> Result<[u8; 43], String> {
     let fvk = FullViewingKey::read(&fvk[..]).expect("valid Sapling FullViewingKey");
-    let dk = zip32::sapling::DiversifierKey::from_bytes(dk);
-    let j = zip32::DiversifierIndex(j);
+    let dk = sapling::zip32::DiversifierKey::from_bytes(dk);
+    let j = zip32::DiversifierIndex::from(j);
 
     sapling_address(&fvk, &dk, j)
         .ok_or_else(|| "Diversifier index does not produce a valid diversifier".to_string())
@@ -114,21 +99,21 @@ fn find_address(
     j: [u8; 11],
 ) -> Result<ffi::FfiPaymentAddress, String> {
     let fvk = FullViewingKey::read(&fvk[..]).expect("valid Sapling FullViewingKey");
-    let dk = zip32::sapling::DiversifierKey::from_bytes(dk);
-    let j = zip32::DiversifierIndex(j);
+    let dk = sapling::zip32::DiversifierKey::from_bytes(dk);
+    let j = zip32::DiversifierIndex::from(j);
 
     sapling_find_address(&fvk, &dk, j)
         .ok_or_else(|| "No valid diversifiers at or above given index".to_string())
         .map(|(j, addr)| ffi::FfiPaymentAddress {
-            j: j.0,
+            j: *j.as_bytes(),
             addr: addr.to_bytes(),
         })
 }
 
 fn diversifier_index(dk: [u8; 32], d: [u8; 11]) -> [u8; 11] {
-    let dk = zip32::sapling::DiversifierKey::from_bytes(dk);
+    let dk = sapling::zip32::DiversifierKey::from_bytes(dk);
     let diversifier = Diversifier(d);
 
     let j = dk.diversifier_index(&diversifier);
-    j.0
+    *j.as_bytes()
 }
diff --git a/src/rust/src/transaction_ffi.rs b/src/rust/src/transaction_ffi.rs
index e10a5e34e8b..95c4ce8dcdd 100644
--- a/src/rust/src/transaction_ffi.rs
+++ b/src/rust/src/transaction_ffi.rs
@@ -6,11 +6,12 @@ use blake2b_simd::Hash;
 use libc::{c_uchar, size_t};
 use tracing::error;
 use zcash_encoding::Vector;
+use zcash_primitives::transaction::components::amount::NonNegativeAmount;
 use zcash_primitives::{
     consensus::BranchId,
     legacy::Script,
     transaction::{
-        components::{sapling, transparent, Amount},
+        components::transparent,
         sighash::{SignableInput, TransparentAuthorizingContext},
         sighash_v5::v5_signature_hash,
         txid::TxIdDigester,
@@ -69,7 +70,7 @@ impl transparent::Authorization for TransparentAuth {
 }
 
 impl TransparentAuthorizingContext for TransparentAuth {
-    fn input_amounts(&self) -> Vec<Amount> {
+    fn input_amounts(&self) -> Vec<NonNegativeAmount> {
         self.all_prev_outputs
             .iter()
             .map(|prevout| prevout.value)
@@ -148,7 +149,7 @@ pub(crate) struct PrecomputedAuth;
 
 impl Authorization for PrecomputedAuth {
     type TransparentAuth = TransparentAuth;
-    type SaplingAuth = sapling::Authorized;
+    type SaplingAuth = sapling::bundle::Authorized;
     type OrchardAuth = orchard::bundle::Authorized;
 }
 
diff --git a/src/rust/src/unified_keys_ffi.rs b/src/rust/src/unified_keys_ffi.rs
index a676f956e7f..c416ecbe604 100644
--- a/src/rust/src/unified_keys_ffi.rs
+++ b/src/rust/src/unified_keys_ffi.rs
@@ -50,9 +50,7 @@ pub extern "C" fn unified_full_viewing_key_parse(
                         Fvk::Sapling(data) => {
                             // The last 32 bytes is the diversifier key, which is opaque.
                             // The remaining 96 bytes should be a valid Sapling FVK.
-                            if zcash_primitives::sapling::keys::FullViewingKey::read(&data[..96])
-                                .is_err()
-                            {
+                            if sapling::keys::FullViewingKey::read(&data[..96]).is_err() {
                                 error!("Unified FVK contains invalid Sapling FVK");
                                 return std::ptr::null_mut();
                             }
diff --git a/src/rust/src/wallet.rs b/src/rust/src/wallet.rs
index 1e800e34460..5253ab9c32d 100644
--- a/src/rust/src/wallet.rs
+++ b/src/rust/src/wallet.rs
@@ -13,7 +13,6 @@ use zcash_encoding::{Optional, Vector};
 use zcash_primitives::{
     consensus::BlockHeight,
     merkle_tree::{read_position, write_position},
-    sapling::NOTE_COMMITMENT_TREE_DEPTH,
     transaction::{components::Amount, TxId},
 };
 
@@ -150,7 +149,8 @@ pub struct Wallet {
     nullifiers: BTreeMap<Nullifier, OutPoint>,
     /// The incremental Merkle tree used to track note commitments and witnesses for notes
     /// belonging to the wallet.
-    commitment_tree: BridgeTree<MerkleHashOrchard, u32, NOTE_COMMITMENT_TREE_DEPTH>,
+    // TODO: Replace this with an `orchard` crate constant (they happen to be the same).
+    commitment_tree: BridgeTree<MerkleHashOrchard, u32, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
     /// The block height at which the last checkpoint was created, if any.
     last_checkpoint: Option<BlockHeight>,
     /// The block height and transaction index of the note most recently added to
@@ -1370,7 +1370,10 @@ pub extern "C" fn orchard_wallet_load_note_commitment_tree(
 #[no_mangle]
 pub extern "C" fn orchard_wallet_init_from_frontier(
     wallet: *mut Wallet,
-    frontier: *const bridgetree::Frontier<MerkleHashOrchard, NOTE_COMMITMENT_TREE_DEPTH>,
+    frontier: *const bridgetree::Frontier<
+        MerkleHashOrchard,
+        { sapling::NOTE_COMMITMENT_TREE_DEPTH },
+    >,
 ) -> bool {
     let wallet = unsafe { wallet.as_mut() }.expect("Wallet pointer may not be null.");
     let frontier = unsafe { frontier.as_ref() }.expect("Wallet pointer may not be null.");
diff --git a/src/rust/src/wallet_scanner.rs b/src/rust/src/wallet_scanner.rs
index 8e4fc9f8860..aa6da98ccc1 100644
--- a/src/rust/src/wallet_scanner.rs
+++ b/src/rust/src/wallet_scanner.rs
@@ -10,15 +10,12 @@ use std::sync::{
 
 use crossbeam_channel as channel;
 use memuse::DynamicUsage;
+use sapling::{bundle::GrothProofBytes, note_encryption::SaplingDomain};
 use zcash_note_encryption::{batch, BatchDomain, Domain, ShieldedOutput, ENC_CIPHERTEXT_SIZE};
 use zcash_primitives::{
     block::BlockHash,
     consensus,
-    sapling::note_encryption::SaplingDomain,
-    transaction::{
-        components::{sapling::GrothProofBytes, OutputDescription},
-        Transaction, TxId,
-    },
+    transaction::{components::OutputDescription, Transaction, TxId},
 };
 
 use crate::{bridge::ffi, note_encryption::parse_and_prepare_sapling_ivk, params::Network};
@@ -38,7 +35,7 @@ trait OutputDomain: BatchDomain {
     const KIND: &'static str;
 }
 
-impl<P: consensus::Parameters> OutputDomain for SaplingDomain<P> {
+impl OutputDomain for SaplingDomain {
     const KIND: &'static str = "sapling";
 }
 
@@ -550,7 +547,7 @@ where
 }
 
 type SaplingRunner =
-    BatchRunner<[u8; 32], SaplingDomain<Network>, OutputDescription<GrothProofBytes>, WithUsage>;
+    BatchRunner<[u8; 32], SaplingDomain, OutputDescription<GrothProofBytes>, WithUsage>;
 
 /// A batch scanner for the `zcashd` wallet.
 pub(crate) struct BatchScanner {
@@ -622,7 +619,7 @@ impl BatchScanner {
             runner.add_outputs(
                 block_tag,
                 txid,
-                || SaplingDomain::for_height(params, height),
+                || SaplingDomain::new(consensus::sapling_zip212_enforcement(&params, height)),
                 bundle.shielded_outputs(),
             );
         }
@@ -671,7 +668,7 @@ impl BatchScanner {
 }
 
 pub(crate) struct BatchResult {
-    sapling: HashMap<(TxId, usize), DecryptedNote<[u8; 32], SaplingDomain<Network>>>,
+    sapling: HashMap<(TxId, usize), DecryptedNote<[u8; 32], SaplingDomain>>,
 }
 
 impl BatchResult {
diff --git a/src/transaction_builder.cpp b/src/transaction_builder.cpp
index 93bc15764e8..a93facf8557 100644
--- a/src/transaction_builder.cpp
+++ b/src/transaction_builder.cpp
@@ -48,11 +48,10 @@ uint256 ProduceShieldedSignatureHash(
 namespace orchard {
 
 Builder::Builder(
-    bool spendsEnabled,
-    bool outputsEnabled,
+    bool coinbase,
     uint256 anchor) : inner(nullptr, orchard_builder_free)
 {
-    inner.reset(orchard_builder_new(spendsEnabled, outputsEnabled, anchor.IsNull() ? nullptr : anchor.begin()));
+    inner.reset(orchard_builder_new(coinbase, anchor.IsNull() ? nullptr : anchor.begin()));
 }
 
 bool Builder::AddSpend(orchard::SpendInfo spendInfo)
@@ -209,6 +208,7 @@ TransactionBuilder::TransactionBuilder(
     const CChainParams& params,
     int nHeight,
     std::optional<uint256> orchardAnchor,
+    uint256 saplingAnchor,
     const CKeyStore* keystore,
     const CCoinsViewCache* coinsView,
     CCriticalSection* cs_coinsView) :
@@ -218,7 +218,8 @@ TransactionBuilder::TransactionBuilder(
     coinsView(coinsView),
     cs_coinsView(cs_coinsView),
     orchardAnchor(orchardAnchor),
-    saplingBuilder(sapling::new_builder(*params.RustNetwork(), nHeight))
+    saplingAnchor(saplingAnchor),
+    saplingBuilder(sapling::new_builder(*params.RustNetwork(), nHeight, saplingAnchor.ToRawBytes(), false))
 {
     mtx = CreateNewContextualCMutableTransaction(
             consensusParams, nHeight,
@@ -226,7 +227,7 @@ TransactionBuilder::TransactionBuilder(
 
     // Ignore the Orchard anchor if we can't use it yet.
     if (orchardAnchor.has_value() && mtx.nVersion >= ZIP225_MIN_TX_VERSION) {
-        orchardBuilder = orchard::Builder(true, true, orchardAnchor.value());
+        orchardBuilder = orchard::Builder(false, orchardAnchor.value());
     }
 }
 
@@ -329,7 +330,6 @@ void TransactionBuilder::AddSaplingSpend(
 
     saplingBuilder->add_spend(
         {reinterpret_cast<uint8_t*>(ssExtSk.data()), ssExtSk.size()},
-        note.d,
         recipient.GetRawBytes(),
         note.value(),
         note.rcm().GetRawBytes(),
@@ -525,7 +525,7 @@ TransactionBuilderResult TransactionBuilder::Build()
 
     std::optional<rust::Box<sapling::UnauthorizedBundle>> maybeSaplingBundle;
     try {
-        maybeSaplingBundle = sapling::build_bundle(std::move(saplingBuilder), nHeight);
+        maybeSaplingBundle = sapling::build_bundle(std::move(saplingBuilder));
     } catch (rust::Error e) {
         return TransactionBuilderResult("Failed to build Sapling bundle: " + std::string(e.what()));
     }
diff --git a/src/transaction_builder.h b/src/transaction_builder.h
index 7645ac87721..4d4121ab6a2 100644
--- a/src/transaction_builder.h
+++ b/src/transaction_builder.h
@@ -91,7 +91,7 @@ class Builder {
     Builder() : inner(nullptr, orchard_builder_free), hasActions(false) { }
 
 public:
-    Builder(bool spendsEnabled, bool outputsEnabled, uint256 anchor);
+    Builder(bool coinbase, uint256 anchor);
 
     // Builder should never be copied
     Builder(const Builder&) = delete;
@@ -242,6 +242,7 @@ class TransactionBuilder
     std::optional<uint256> orchardAnchor;
     std::optional<orchard::Builder> orchardBuilder;
     CAmount valueBalanceOrchard = 0;
+    uint256 saplingAnchor;
     rust::Box<sapling::Builder> saplingBuilder;
     CAmount valueBalanceSapling = 0;
 
@@ -268,6 +269,7 @@ class TransactionBuilder
         const CChainParams& params,
         int nHeight,
         std::optional<uint256> orchardAnchor,
+        uint256 saplingAnchor,
         const CKeyStore* keyStore = nullptr,
         const CCoinsViewCache* coinsView = nullptr,
         CCriticalSection* cs_coinsView = nullptr);
@@ -286,6 +288,7 @@ class TransactionBuilder
         orchardAnchor(std::move(builder.orchardAnchor)),
         orchardBuilder(std::move(builder.orchardBuilder)),
         valueBalanceOrchard(std::move(builder.valueBalanceOrchard)),
+        saplingAnchor(std::move(builder.saplingAnchor)),
         saplingBuilder(std::move(builder.saplingBuilder)),
         valueBalanceSapling(std::move(builder.valueBalanceSapling)),
         orchardSpendingKeys(std::move(orchardSpendingKeys)),
@@ -308,8 +311,10 @@ class TransactionBuilder
             cs_coinsView = std::move(builder.cs_coinsView);
             mtx = std::move(builder.mtx);
             fee = std::move(builder.fee);
+            orchardAnchor = std::move(builder.orchardAnchor);
             orchardBuilder = std::move(builder.orchardBuilder);
             valueBalanceOrchard = std::move(builder.valueBalanceOrchard);
+            saplingAnchor = std::move(builder.saplingAnchor);
             saplingBuilder = std::move(builder.saplingBuilder);
             valueBalanceSapling = std::move(builder.valueBalanceSapling);
             orchardSpendingKeys = std::move(builder.orchardSpendingKeys),
diff --git a/src/wallet/asyncrpcoperation_saplingmigration.cpp b/src/wallet/asyncrpcoperation_saplingmigration.cpp
index a30281db894..19052d8c436 100644
--- a/src/wallet/asyncrpcoperation_saplingmigration.cpp
+++ b/src/wallet/asyncrpcoperation_saplingmigration.cpp
@@ -16,7 +16,8 @@
 
 const int MIGRATION_EXPIRY_DELTA = 450;
 
-AsyncRPCOperation_saplingmigration::AsyncRPCOperation_saplingmigration(int targetHeight) : targetHeight_(targetHeight) {}
+AsyncRPCOperation_saplingmigration::AsyncRPCOperation_saplingmigration(int targetHeight, uint256 saplingAnchor) :
+    targetHeight_(targetHeight), saplingAnchor_(saplingAnchor) {}
 
 AsyncRPCOperation_saplingmigration::~AsyncRPCOperation_saplingmigration() {}
 
@@ -112,7 +113,14 @@ bool AsyncRPCOperation_saplingmigration::main_impl() {
     CCoinsViewCache coinsView(pcoinsTip);
     do {
         CAmount amountToSend = chooseAmount(availableFunds);
-        auto builder = TransactionBuilder(Params(), targetHeight_, std::nullopt, pwalletMain, &coinsView, &cs_main);
+        auto builder = TransactionBuilder(
+            Params(),
+            targetHeight_,
+            std::nullopt,
+            saplingAnchor_,
+            pwalletMain,
+            &coinsView,
+            &cs_main);
         builder.SetExpiryHeight(targetHeight_ + MIGRATION_EXPIRY_DELTA);
         LogPrint("zrpcunsafe", "%s: Beginning creating transaction with Sapling output amount=%s\n", getId(), FormatMoney(amountToSend - LEGACY_DEFAULT_FEE));
         std::vector<SproutNoteEntry> fromNotes;
diff --git a/src/wallet/asyncrpcoperation_saplingmigration.h b/src/wallet/asyncrpcoperation_saplingmigration.h
index 7d1d085efa1..be39eef7983 100644
--- a/src/wallet/asyncrpcoperation_saplingmigration.h
+++ b/src/wallet/asyncrpcoperation_saplingmigration.h
@@ -9,7 +9,7 @@
 class AsyncRPCOperation_saplingmigration : public AsyncRPCOperation
 {
 public:
-    AsyncRPCOperation_saplingmigration(int targetHeight);
+    AsyncRPCOperation_saplingmigration(int targetHeight, uint256 saplingAnchor);
     virtual ~AsyncRPCOperation_saplingmigration();
 
     // We don't want to be copied or moved around
@@ -28,6 +28,7 @@ class AsyncRPCOperation_saplingmigration : public AsyncRPCOperation
 
 private:
     int targetHeight_;
+    uint256 saplingAnchor_;
 
     bool main_impl();
 
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index cb0bfdc8333..1bcb749b427 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -1481,7 +1481,11 @@ void CWallet::RunSaplingMigration(int blockHeight) {
             lastOperation->cancel();
         }
         pendingSaplingMigrationTxs.clear();
-        std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_saplingmigration(blockHeight + 5));
+        auto targetHeight = blockHeight + 5;
+        auto anchorBlockIndex = chainActive[blockHeight - 5];
+        assert(anchorBlockIndex != nullptr);
+        auto saplingAnchor = anchorBlockIndex->hashFinalSaplingRoot;
+        std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_saplingmigration(targetHeight, saplingAnchor));
         saplingMigrationOperationId = operation->getId();
         q->addOperation(operation);
     } else if (blockHeight % 500 == 499) {
diff --git a/src/wallet/wallet_tx_builder.cpp b/src/wallet/wallet_tx_builder.cpp
index 6b84589cea4..1e52df0bbdc 100644
--- a/src/wallet/wallet_tx_builder.cpp
+++ b/src/wallet/wallet_tx_builder.cpp
@@ -937,9 +937,6 @@ TransactionBuilderResult TransactionEffects::ApproveAndBuild(
         orchardAnchor = anchorBlockIndex->hashFinalOrchardRoot;
     }
 
-    auto builder = TransactionBuilder(params, nextBlockHeight, orchardAnchor, &wallet);
-    builder.SetFee(fee);
-
     // Track the total of notes that we've added to the builder. This
     // shouldn't strictly be necessary, given `spendable.LimitToAmount`
     CAmount totalSpend = 0;
@@ -984,6 +981,9 @@ TransactionBuilderResult TransactionEffects::ApproveAndBuild(
         }
     }
 
+    auto builder = TransactionBuilder(params, nextBlockHeight, orchardAnchor, saplingAnchor, &wallet);
+    builder.SetFee(fee);
+
     // Add Orchard spends
     for (size_t i = 0; i < orchardSpendInfo.size(); i++) {
         auto spendInfo = std::move(orchardSpendInfo[i]);
diff --git a/src/zcash/address/zip32.cpp b/src/zcash/address/zip32.cpp
index 703d64f295f..f696b317d42 100644
--- a/src/zcash/address/zip32.cpp
+++ b/src/zcash/address/zip32.cpp
@@ -55,28 +55,6 @@ std::optional<uint32_t> diversifier_index_t::ToTransparentChildIndex() const {
     }
 }
 
-//
-// SaplingExtendedFullViewingKey
-//
-
-std::optional<SaplingExtendedFullViewingKey> SaplingExtendedFullViewingKey::Derive(uint32_t i) const
-{
-    CDataStream ss_p(SER_NETWORK, PROTOCOL_VERSION);
-    ss_p << *this;
-    std::array<unsigned char, ZIP32_XFVK_SIZE> p_bytes;
-    std::copy(ss_p.begin(), ss_p.end(), p_bytes.begin());
-
-    try {
-        auto i_bytes = sapling::zip32::xfvk_derive(p_bytes, i);
-        CDataStream ss_i(i_bytes, SER_NETWORK, PROTOCOL_VERSION);
-        SaplingExtendedFullViewingKey xfvk_i;
-        ss_i >> xfvk_i;
-        return xfvk_i;
-    } catch (rust::Error) {
-        return std::nullopt;
-    }
-}
-
 //
 // SaplingDiversifiableFullViewingKey
 //
@@ -173,6 +151,10 @@ SaplingExtendedSpendingKey SaplingExtendedSpendingKey::Master(const HDSeed& seed
 
 SaplingExtendedSpendingKey SaplingExtendedSpendingKey::Derive(uint32_t i) const
 {
+    if (i < HARDENED_KEY_LIMIT) {
+        throw std::runtime_error("non-hardened derivation is unsupported");
+    }
+
     CDataStream ss_p(SER_NETWORK, PROTOCOL_VERSION);
     ss_p << *this;
     std::array<unsigned char, ZIP32_XSK_SIZE> p_bytes;
diff --git a/src/zcash/address/zip32.h b/src/zcash/address/zip32.h
index c6ba85dda72..185f34602c4 100644
--- a/src/zcash/address/zip32.h
+++ b/src/zcash/address/zip32.h
@@ -257,8 +257,6 @@ class SaplingExtendedFullViewingKey: public SaplingDiversifiableFullViewingKey {
         READWRITE(dk);
     }
 
-    std::optional<SaplingExtendedFullViewingKey> Derive(uint32_t i) const;
-
     friend inline bool operator==(const SaplingExtendedFullViewingKey& a, const SaplingExtendedFullViewingKey& b) {
         return (
             a.depth == b.depth &&
diff --git a/src/zcbenchmarks.cpp b/src/zcbenchmarks.cpp
index e784ace1f63..cef9ea61c94 100644
--- a/src/zcbenchmarks.cpp
+++ b/src/zcbenchmarks.cpp
@@ -759,6 +759,7 @@ double benchmark_create_sapling_spend()
     auto maybe_cmu = note.cmu();
     tree.append(maybe_cmu.value());
     auto witness = tree.witness();
+    auto anchor = tree.root().GetRawBytes();
 
     CDataStream ssExtSk(SER_NETWORK, PROTOCOL_VERSION);
     ssExtSk << sk;
@@ -769,10 +770,9 @@ double benchmark_create_sapling_spend()
     std::move(ss.begin(), ss.end(), witnessChars.begin());
 
     auto nHeight = Params().GetConsensus().vUpgrades[Consensus::UPGRADE_SAPLING].nActivationHeight;
-    auto builder = sapling::new_builder(*Params().RustNetwork(), nHeight);
+    auto builder = sapling::new_builder(*Params().RustNetwork(), nHeight, anchor, false);
     builder->add_spend(
         {reinterpret_cast<uint8_t*>(ssExtSk.data()), ssExtSk.size()},
-        note.d,
         address.GetRawBytes(),
         note.value(),
         note.rcm().GetRawBytes(),
@@ -781,7 +781,7 @@ double benchmark_create_sapling_spend()
     struct timeval tv_start;
     timer_start(tv_start);
 
-    auto result = sapling::build_bundle(std::move(builder), nHeight);
+    auto result = sapling::build_bundle(std::move(builder));
 
     double t = timer_stop(tv_start);
     return t;
@@ -791,9 +791,10 @@ double benchmark_create_sapling_output()
 {
     auto sk = libzcash::SaplingSpendingKey::random();
     auto address = sk.default_address();
+    auto anchor = SaplingMerkleTree::empty_root().GetRawBytes();
 
     auto nHeight = Params().GetConsensus().vUpgrades[Consensus::UPGRADE_SAPLING].nActivationHeight;
-    auto builder = sapling::new_builder(*Params().RustNetwork(), nHeight);
+    auto builder = sapling::new_builder(*Params().RustNetwork(), nHeight, anchor, false);
     builder->add_recipient(
         uint256().GetRawBytes(),
         address.GetRawBytes(),
@@ -803,7 +804,7 @@ double benchmark_create_sapling_output()
     struct timeval tv_start;
     timer_start(tv_start);
 
-    auto result = sapling::build_bundle(std::move(builder), nHeight);
+    auto result = sapling::build_bundle(std::move(builder));
 
     double t = timer_stop(tv_start);
     return t;


Test patch:
diff --git a/qa/rpc-tests/finalorchardroot.py b/qa/rpc-tests/finalorchardroot.py
index 33d6605a4a0..d5a478b8dfc 100755
--- a/qa/rpc-tests/finalorchardroot.py
+++ b/qa/rpc-tests/finalorchardroot.py
@@ -237,7 +237,7 @@ def run_test(self):
         assert new_treestate["sapling"]["commitments"]["finalRoot"] != treestate["sapling"]["commitments"]["finalRoot"]
         assert new_treestate["sapling"]["commitments"]["finalState"] != treestate["sapling"]["commitments"]["finalState"]
         assert_equal(len(new_treestate["sapling"]["commitments"]["finalRoot"]), 64)
-        assert_equal(len(new_treestate["sapling"]["commitments"]["finalState"]), 70)
+        assert_equal(len(new_treestate["sapling"]["commitments"]["finalState"]), 134)
         treestate = new_treestate
 
         # Mine a block with an Orchard shielded recipient and verify the final Orchard root changes
diff --git a/qa/rpc-tests/finalsaplingroot.py b/qa/rpc-tests/finalsaplingroot.py
index 7bb38241d2f..ed4914ac749 100755
--- a/qa/rpc-tests/finalsaplingroot.py
+++ b/qa/rpc-tests/finalsaplingroot.py
@@ -122,8 +122,8 @@ def run_test(self):
 
         # Verify there is a Sapling output description (its commitment was added to tree)
         result = self.nodes[0].getrawtransaction(mytxid, 1)
-        assert_equal(len(result["vShieldedOutput"]), 1)
-        assert_equal(blk["trees"]["sapling"]["size"], 1)
+        assert_equal(len(result["vShieldedOutput"]), 2) # Non-coinbase bundles are padded
+        assert_equal(blk["trees"]["sapling"]["size"], 2)
 
         # Since there is a now sapling shielded input in the blockchain,
         # the sapling values should have changed
@@ -133,7 +133,7 @@ def run_test(self):
         assert new_treestate["sapling"]["commitments"]["finalRoot"] != treestate["sapling"]["commitments"]["finalRoot"]
         assert new_treestate["sapling"]["commitments"]["finalState"] != treestate["sapling"]["commitments"]["finalState"]
         assert_equal(len(new_treestate["sapling"]["commitments"]["finalRoot"]), 64)
-        assert_equal(len(new_treestate["sapling"]["commitments"]["finalState"]), 70)
+        assert_equal(len(new_treestate["sapling"]["commitments"]["finalState"]), 134)
         treestate = new_treestate
 
         # Mine an empty block and verify the final Sapling root does not change
@@ -142,7 +142,7 @@ def run_test(self):
         self.sync_all()
         blk = self.nodes[0].getblock("202")
         assert_equal(root, blk["finalsaplingroot"])
-        assert_equal(blk["trees"]["sapling"]["size"], 1)
+        assert_equal(blk["trees"]["sapling"]["size"], 2)
 
         # Mine a block with a transparent tx and verify the final Sapling root does not change
         taddr1 = self.nodes[1].getnewaddress()
@@ -171,7 +171,7 @@ def run_test(self):
         assert_equal(len(blk["tx"]), 2)
         assert_equal(self.nodes[0].z_getbalance(zaddr0), Decimal("37.66"))
         assert_equal(root, blk["finalsaplingroot"])
-        assert_equal(blk["trees"]["sapling"]["size"], 1)
+        assert_equal(blk["trees"]["sapling"]["size"], 2)
 
         new_treestate = self.nodes[0].z_gettreestate(str(-1))
         assert_equal(new_treestate["sapling"]["commitments"]["finalRoot"], root)
@@ -200,14 +200,14 @@ def run_test(self):
         # Verify there is a Sapling output description (its commitment was added to tree)
         result = self.nodes[0].getrawtransaction(mytxid, 1)
         assert_equal(len(result["vShieldedOutput"]), 2)  # there is Sapling shielded change
-        assert_equal(blk["trees"]["sapling"]["size"], 3)
+        assert_equal(blk["trees"]["sapling"]["size"], 4)
 
         new_treestate = self.nodes[0].z_gettreestate(str(-1))
         assert_equal(new_treestate["sprout"], treestate["sprout"])
         assert new_treestate["sapling"]["commitments"]["finalRoot"] != treestate["sapling"]["commitments"]["finalRoot"]
         assert new_treestate["sapling"]["commitments"]["finalState"] != treestate["sapling"]["commitments"]["finalState"]
         assert_equal(len(new_treestate["sapling"]["commitments"]["finalRoot"]), 64)
-        assert_equal(len(new_treestate["sapling"]["commitments"]["finalState"]), 136)
+        assert_equal(len(new_treestate["sapling"]["commitments"]["finalState"]), 200)
         treestate = new_treestate
 
         # Mine a block with a Sapling shielded sender and transparent recipient.
@@ -221,16 +221,19 @@ def run_test(self):
         self.nodes[0].generate(1)
         self.sync_all()
 
-        assert_equal(len(self.nodes[0].getblock("206")["tx"]), 2)
+        blk = self.nodes[0].getblock("206")
+        assert_equal(len(blk["tx"]), 2)
+        assert_equal(mytxid, blk["tx"][1])
         assert_equal(self.nodes[0].z_getbalance(taddr2), Decimal("2.34"))
         assert_equal(self.nodes[1].z_getbalance(saplingAddr1), 0)
 
         # Verify the final Sapling root changes (because the Sapling bundle was padded
         # with 2 dummy outputs).
-        blk = self.nodes[0].getblock("206")
+        print(self.nodes[0].getrawtransaction(blk["tx"][0]))
+        print(self.nodes[0].getrawtransaction(blk["tx"][1]))
         root = blk["finalsaplingroot"]
         assert root != self.nodes[0].getblock("205")["finalsaplingroot"]
-        assert_equal(blk["trees"]["sapling"]["size"], 5)
+        assert_equal(blk["trees"]["sapling"]["size"], 6)
 
         # Verify there are two Sapling output descriptions.
         result = self.nodes[0].getrawtransaction(mytxid, 1)
@@ -241,7 +244,7 @@ def run_test(self):
         assert new_treestate["sapling"]["commitments"]["finalRoot"] != treestate["sapling"]["commitments"]["finalRoot"]
         assert new_treestate["sapling"]["commitments"]["finalState"] != treestate["sapling"]["commitments"]["finalState"]
         assert_equal(len(new_treestate["sapling"]["commitments"]["finalRoot"]), 64)
-        assert_equal(len(new_treestate["sapling"]["commitments"]["finalState"]), 138)
+        assert_equal(len(new_treestate["sapling"]["commitments"]["finalState"]), 202)
         treestate = new_treestate
 
         # Activate NU5; more testing should be added once we can mine orchard transactions.
diff --git a/src/gtest/test_checktransaction.cpp b/src/gtest/test_checktransaction.cpp
index d3ccca24b4d..479e230ae96 100644
--- a/src/gtest/test_checktransaction.cpp
+++ b/src/gtest/test_checktransaction.cpp
@@ -325,6 +325,8 @@ TEST(ChecktransactionTests, BadTxnsTxouttotalToolargeOutputs) {
     CheckTransactionWithoutProofVerification(tx, state);
 }
 
+// TODO: The new Sapling bundle API prevents us from constructing this case.
+/*
 TEST(ChecktransactionTests, ValueBalanceNonZero) {
     CMutableTransaction mtx = GetValidTransaction(NetworkUpgradeInfo[Consensus::UPGRADE_SAPLING].nBranchId);
     mtx.saplingBundle = sapling::test_only_invalid_bundle(0, 0, 10);
@@ -333,8 +335,9 @@ TEST(ChecktransactionTests, ValueBalanceNonZero) {
 
     MockCValidationState state;
     EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-valuebalance-nonzero", false, "")).Times(1);
-    CheckTransactionWithoutProofVerification(tx, state);
+    EXPECT_FALSE(CheckTransactionWithoutProofVerification(tx, state));
 }
+*/
 
 TEST(ChecktransactionTests, ValueBalanceOverflowsTotal) {
     CMutableTransaction mtx = GetValidTransaction(NetworkUpgradeInfo[Consensus::UPGRADE_SAPLING].nBranchId);
@@ -1152,7 +1155,8 @@ TEST(ChecktransactionTests, HeartwoodAcceptsSaplingShieldedCoinbase) {
     RegtestActivateHeartwood(false, Consensus::NetworkUpgrade::ALWAYS_ACTIVE);
     auto chainparams = Params();
 
-    auto builder = sapling::new_builder(*chainparams.RustNetwork(), 10);
+    auto saplingAnchor = SaplingMerkleTree::empty_root().GetRawBytes();
+    auto builder = sapling::new_builder(*chainparams.RustNetwork(), 10, saplingAnchor, true);
     builder->add_recipient(
         uint256().GetRawBytes(),
         libzcash::SaplingSpendingKey::random().default_address().GetRawBytes(),
@@ -1167,7 +1171,7 @@ TEST(ChecktransactionTests, HeartwoodAcceptsSaplingShieldedCoinbase) {
     mtx.vin.resize(1);
     mtx.vin[0].prevout.SetNull();
     mtx.vJoinSplit.resize(0);
-    mtx.saplingBundle = sapling::apply_bundle_signatures(sapling::build_bundle(std::move(builder), 10), {});
+    mtx.saplingBundle = sapling::apply_bundle_signatures(sapling::build_bundle(std::move(builder)), {});
     auto outputs = mtx.saplingBundle.GetDetails().outputs();
     auto& odesc = outputs[0];
 
@@ -1256,10 +1260,12 @@ TEST(ChecktransactionTests, HeartwoodEnforcesSaplingRulesOnShieldedCoinbase) {
     mtx.vin[0].prevout.SetNull();
     mtx.vin[0].scriptSig << 123;
     mtx.vJoinSplit.resize(0);
-    mtx.saplingBundle = sapling::test_only_invalid_bundle(0, 0, -1000);
+    mtx.saplingBundle = sapling::test_only_invalid_bundle(0, 1, -1000);
 
     // Coinbase transaction should fail non-contextual checks with no shielded
     // outputs and non-zero valueBalanceSapling.
+    // TODO: The new Sapling bundle API prevents us from constructing this case.
+    /*
     {
         CTransaction tx(mtx);
         EXPECT_TRUE(tx.IsCoinBase());
@@ -1268,15 +1274,17 @@ TEST(ChecktransactionTests, HeartwoodEnforcesSaplingRulesOnShieldedCoinbase) {
         EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-valuebalance-nonzero", false, "")).Times(1);
         EXPECT_FALSE(CheckTransactionWithoutProofVerification(tx, state));
     }
+    */
 
     // Add a Sapling output.
-    auto builder = sapling::new_builder(*chainparams.RustNetwork(), 10);
+    auto saplingAnchor = SaplingMerkleTree::empty_root().GetRawBytes();
+    auto builder = sapling::new_builder(*chainparams.RustNetwork(), 10, saplingAnchor, true);
     builder->add_recipient(
         uint256().GetRawBytes(),
         libzcash::SaplingSpendingKey::random().default_address().GetRawBytes(),
         1000,
         libzcash::Memo::ToBytes(std::nullopt));
-    mtx.saplingBundle = sapling::apply_bundle_signatures(sapling::build_bundle(std::move(builder), 10), {});
+    mtx.saplingBundle = sapling::apply_bundle_signatures(sapling::build_bundle(std::move(builder)), {});
 
     CTransaction tx(mtx);
     EXPECT_TRUE(tx.IsCoinBase());
@@ -1376,9 +1384,15 @@ TEST(ChecktransactionTests, InvalidOrchardShieldedCoinbase) {
     mtx.nConsensusBranchId = NetworkUpgradeInfo[Consensus::UPGRADE_NU5].nBranchId;
 
     // Make it an invalid shielded coinbase, by creating an all-dummy Orchard bundle.
+    RawHDSeed seed(32, 0);
+    auto to = libzcash::OrchardSpendingKey::ForAccount(seed, 133, 0)
+        .ToFullViewingKey()
+        .GetChangeAddress();
     mtx.vin.resize(1);
     mtx.vin[0].prevout.SetNull();
-    mtx.orchardBundle = orchard::Builder(false, true, uint256())
+    auto builder = orchard::Builder(true, uint256());
+    builder.AddOutput(std::nullopt, to, 0, std::nullopt);
+    mtx.orchardBundle = builder
         .Build().value()
         .ProveAndSign({}, uint256()).value();
 
@@ -1405,7 +1419,7 @@ TEST(ChecktransactionTests, NU5AcceptsOrchardShieldedCoinbase) {
     auto chainparams = Params();
 
     uint256 orchardAnchor;
-    auto builder = orchard::Builder(false, true, orchardAnchor);
+    auto builder = orchard::Builder(true, orchardAnchor);
 
     // Shielded coinbase outputs must be recoverable with an all-zeroes ovk.
     RawHDSeed rawSeed(32, 0);
@@ -1527,7 +1541,7 @@ TEST(ChecktransactionTests, NU5EnforcesOrchardRulesOnShieldedCoinbase) {
     auto chainparams = Params();
 
     uint256 orchardAnchor;
-    auto builder = orchard::Builder(false, true, orchardAnchor);
+    auto builder = orchard::Builder(true, orchardAnchor);
 
     // Shielded coinbase outputs must be recoverable with an all-zeroes ovk.
     RawHDSeed rawSeed(32, 0);
diff --git a/src/gtest/test_coins.cpp b/src/gtest/test_coins.cpp
index 6302817c399..53d5c674b40 100644
--- a/src/gtest/test_coins.cpp
+++ b/src/gtest/test_coins.cpp
@@ -448,11 +448,14 @@ class TxWithNullifiers
         mutableTxV5.saplingBundle = sapling::test_only_invalid_bundle(1, 0, 0);
         saplingNullifier = uint256::FromRawBytes(mutableTxV5.saplingBundle.GetDetails().spends()[0].nullifier());
 
-        // The Orchard bundle builder always pads to two Actions, so we can just
-        // use an empty builder to create a dummy Orchard bundle.
+        RawHDSeed seed(32, 0);
+        auto to = libzcash::OrchardSpendingKey::ForAccount(seed, 133, 0)
+            .ToFullViewingKey()
+            .GetChangeAddress();
         uint256 orchardAnchor;
         uint256 dataToBeSigned;
-        auto builder = orchard::Builder(true, true, orchardAnchor);
+        auto builder = orchard::Builder(false, orchardAnchor);
+        builder.AddOutput(std::nullopt, to, 0, std::nullopt);
         mutableTxV5.orchardBundle = builder.Build().value().ProveAndSign({}, dataToBeSigned).value();
         orchardNullifier = mutableTxV5.orchardBundle.GetNullifiers().at(0);
 
diff --git a/src/gtest/test_dynamicusage.cpp b/src/gtest/test_dynamicusage.cpp
index 26e425524c9..62df81888ad 100644
--- a/src/gtest/test_dynamicusage.cpp
+++ b/src/gtest/test_dynamicusage.cpp
@@ -23,7 +23,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionTransparent)
     auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID());
     CTxDestination taddr = tsk.GetPubKey().GetID();
 
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt, &keystore);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, SaplingMerkleTree::empty_root(), &keystore);
     builder.SetFee(10000);
     builder.AddTransparentInput(
         COutPoint(uint256S("7777777777777777777777777777777777777777777777777777777777777777"), 0),
@@ -64,7 +64,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionSaplingToSapling)
     auto pa = extfvk.DefaultAddress();
     auto testNote = GetTestSaplingNote(pa, 50000);
 
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root());
     builder.SetFee(10000);
     builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
     builder.AddSaplingOutput(fvk.ovk, pa, 5000, {});
@@ -73,7 +73,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionSaplingToSapling)
     // 1 vShieldedSpend + 2 vShieldedOutput
     EXPECT_EQ(1, tx.GetSaplingSpendsCount());
     EXPECT_EQ(2, tx.GetSaplingOutputsCount());
-    EXPECT_EQ(400 + 2520, RecursiveDynamicUsage(tx));
+    EXPECT_EQ(400 + 32 + 2520, RecursiveDynamicUsage(tx));
 
     RegtestDeactivateSapling();
 }
@@ -89,7 +89,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionTransparentToSapling)
     auto scriptPubKey = GetScriptForDestination(tsk.GetPubKey().GetID());
     auto sk = libzcash::SaplingSpendingKey::random();
 
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt, &keystore);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, SaplingMerkleTree::empty_root(), &keystore);
     builder.SetFee(10000);
     builder.AddTransparentInput(
         COutPoint(uint256S("7777777777777777777777777777777777777777777777777777777777777777"), 0),
@@ -97,10 +97,10 @@ TEST(RecursiveDynamicUsageTests, TestTransactionTransparentToSapling)
     builder.AddSaplingOutput(sk.full_viewing_key().ovk, sk.default_address(), 40000, {});
 
     auto tx = builder.Build().GetTxOrThrow();
-    // 1 vin + 1 vShieldedOutput
+    // 1 vin + 2 vShieldedOutput
     EXPECT_EQ(0, tx.GetSaplingSpendsCount());
-    EXPECT_EQ(1, tx.GetSaplingOutputsCount());
-    EXPECT_EQ((96 + 128) + 1200, RecursiveDynamicUsage(tx));
+    EXPECT_EQ(2, tx.GetSaplingOutputsCount());
+    EXPECT_EQ((96 + 128) + 2280, RecursiveDynamicUsage(tx));
 
     RegtestDeactivateSapling();
 }
@@ -117,7 +117,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionSaplingToTransparent)
     auto sk = GetTestMasterSaplingSpendingKey();
     auto testNote = GetTestSaplingNote(sk.ToXFVK().DefaultAddress(), 50000);
 
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt, &keystore);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root(), &keystore);
     builder.SetFee(10000);
     builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
     builder.AddTransparentOutput(taddr, 40000);
@@ -126,7 +126,7 @@ TEST(RecursiveDynamicUsageTests, TestTransactionSaplingToTransparent)
     // 1 vShieldedSpend + 2 vShieldedOutput + 1 vout
     EXPECT_EQ(1, tx.GetSaplingSpendsCount());
     EXPECT_EQ(2, tx.GetSaplingOutputsCount());
-    EXPECT_EQ(400 + 2520 + 64, RecursiveDynamicUsage(tx));
+    EXPECT_EQ(400 + 2520 + 64 + 32, RecursiveDynamicUsage(tx));
 
     RegtestDeactivateSapling();
 }
diff --git a/src/gtest/test_keys.cpp b/src/gtest/test_keys.cpp
index 0482e5fa518..7058d3caf19 100644
--- a/src/gtest/test_keys.cpp
+++ b/src/gtest/test_keys.cpp
@@ -20,7 +20,7 @@ TEST(Keys, EncodeAndDecodeSapling)
     auto m = GetTestMasterSaplingSpendingKey();
 
     for (uint32_t i = 0; i < 1000; i++) {
-        auto sk = m.Derive(i);
+        auto sk = m.Derive(i | HARDENED_KEY_LIMIT);
         {
             std::string sk_string = keyIO.EncodeSpendingKey(sk);
             EXPECT_EQ(
diff --git a/src/gtest/test_mempoollimit.cpp b/src/gtest/test_mempoollimit.cpp
index 9abffbf2d4c..b0952d0d329 100644
--- a/src/gtest/test_mempoollimit.cpp
+++ b/src/gtest/test_mempoollimit.cpp
@@ -124,7 +124,7 @@ TEST(MempoolLimitTests, MempoolCostAndEvictionWeight)
 
     // Default fee
     {
-        auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+        auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root());
         builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
         builder.AddSaplingOutput(fvk.ovk, pa, 25000, {});
 
@@ -135,7 +135,7 @@ TEST(MempoolLimitTests, MempoolCostAndEvictionWeight)
 
     // Lower than standard fee
     {
-        auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+        auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root());
         builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
         builder.AddSaplingOutput(fvk.ovk, pa, 25000, {});
         static_assert(MINIMUM_FEE == 10000);
@@ -148,7 +148,7 @@ TEST(MempoolLimitTests, MempoolCostAndEvictionWeight)
 
     // Larger Tx
     {
-        auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+        auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root());
         builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
         for (int i = 0; i < 10; i++) {
             builder.AddSaplingOutput(fvk.ovk, pa, 1000, {});
diff --git a/src/gtest/test_transaction_builder.cpp b/src/gtest/test_transaction_builder.cpp
index 82fabc0bddf..2986d696f2f 100644
--- a/src/gtest/test_transaction_builder.cpp
+++ b/src/gtest/test_transaction_builder.cpp
@@ -40,7 +40,7 @@ TEST(TransactionBuilder, TransparentToSapling)
 
     // Create a shielding transaction from transparent to Sapling
     // 0.00005 t-ZEC in, 0.00004 z-ZEC out, default fee
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt, &keystore);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, SaplingMerkleTree::empty_root(), &keystore);
     builder.AddTransparentInput(COutPoint(uint256S("1234"), 0), scriptPubKey, 5000);
     builder.AddSaplingOutput(fvk_from.ovk, pk, 4000, {});
     auto tx = builder.Build().GetTxOrThrow();
@@ -49,7 +49,7 @@ TEST(TransactionBuilder, TransparentToSapling)
     EXPECT_EQ(tx.vout.size(), 0);
     EXPECT_EQ(tx.vJoinSplit.size(), 0);
     EXPECT_EQ(tx.GetSaplingSpendsCount(), 0);
-    EXPECT_EQ(tx.GetSaplingOutputsCount(), 1);
+    EXPECT_EQ(tx.GetSaplingOutputsCount(), 2);
     EXPECT_EQ(tx.GetValueBalanceSapling(), -4000);
 
     CValidationState state;
@@ -74,7 +74,7 @@ TEST(TransactionBuilder, SaplingToSapling) {
 
     // Create a Sapling-only transaction
     // 0.00004 z-ZEC in, 0.000025 z-ZEC out, default fee, 0.000005 z-ZEC change
-    auto builder = TransactionBuilder(Params(), 2, std::nullopt);
+    auto builder = TransactionBuilder(Params(), 2, std::nullopt, testNote.tree.root());
     builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
 
     // Check that trying to add a different anchor fails
@@ -119,7 +119,7 @@ TEST(TransactionBuilder, SaplingToSprout) {
     // - 0.00004 Sapling-ZEC in     - 0.000025 Sprout-ZEC out
     //                              - 0.000005 Sapling-ZEC change
     //                              - default t-ZEC fee
-    auto builder = TransactionBuilder(Params(), 2, std::nullopt, nullptr);
+    auto builder = TransactionBuilder(Params(), 2, std::nullopt, testNote.tree.root(), nullptr);
     builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
     builder.AddSproutOutput(sproutAddr, 2500, std::nullopt);
     auto tx = builder.Build().GetTxOrThrow();
@@ -173,7 +173,7 @@ TEST(TransactionBuilder, SproutToSproutAndSapling) {
     //                              - 0.00005 Sprout-ZEC change
     //                              - 0.00005 Sapling-ZEC out
     //                              - 0.00005 t-ZEC fee
-    auto builder = TransactionBuilder(Params(), 2, std::nullopt, nullptr, &view);
+    auto builder = TransactionBuilder(Params(), 2, std::nullopt, SaplingMerkleTree::empty_root(), nullptr, &view);
     builder.SetFee(5000);
     builder.AddSproutInput(sproutSk, sproutNote, sproutWitness);
     builder.AddSproutOutput(sproutAddr, 6000, std::nullopt);
@@ -193,7 +193,7 @@ TEST(TransactionBuilder, SproutToSproutAndSapling) {
     EXPECT_EQ(tx.vJoinSplit[2].vpub_old, 0);
     EXPECT_EQ(tx.vJoinSplit[2].vpub_new, 10000);
     EXPECT_EQ(tx.GetSaplingSpendsCount(), 0);
-    EXPECT_EQ(tx.GetSaplingOutputsCount(), 1);
+    EXPECT_EQ(tx.GetSaplingOutputsCount(), 2);
     EXPECT_EQ(tx.GetValueBalanceSapling(), -5000);
 
     CValidationState state;
@@ -247,7 +247,7 @@ TEST(TransactionBuilder, TransparentToOrchard)
 
     // Create a shielding transaction from transparent to Orchard
     // 0.00005 t-ZEC in, 0.00004 z-ZEC out, default fee
-    auto builder = TransactionBuilder(Params(), 1, orchardAnchor, &keystore);
+    auto builder = TransactionBuilder(Params(), 1, orchardAnchor, SaplingMerkleTree::empty_root(), &keystore);
     builder.AddTransparentInput(COutPoint(uint256S("1234"), 0), scriptPubKey, 5000);
     builder.AddOrchardOutput(std::nullopt, recipient, 4000, std::nullopt);
     auto maybeTx = builder.Build();
@@ -279,7 +279,7 @@ TEST(TransactionBuilder, ThrowsOnTransparentInputWithoutKeyStore)
     SelectParams(CBaseChainParams::REGTEST);
     const Consensus::Params& consensusParams = Params().GetConsensus();
 
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, SaplingMerkleTree::empty_root());
     ASSERT_THROW(builder.AddTransparentInput(COutPoint(), CScript(), 1), std::runtime_error);
 }
 
@@ -290,7 +290,7 @@ TEST(TransactionBuilder, RejectsInvalidTransparentOutput)
 
     // Default CTxDestination type is an invalid address
     CTxDestination taddr;
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, SaplingMerkleTree::empty_root());
     ASSERT_THROW(builder.AddTransparentOutput(taddr, 50), UniValue);
 }
 
@@ -317,13 +317,13 @@ TEST(TransactionBuilder, FailsWithNegativeChange)
 
     // Fail if there is only a Sapling output
     // 0.00005 z-ZEC out, default fee
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root());
     builder.AddSaplingOutput(fvk.ovk, pa, 5000, {});
     EXPECT_EQ("Change cannot be negative: -0.00006 ZEC", builder.Build().GetError());
 
     // Fail if there is only a transparent output
     // 0.00005 t-ZEC out, default fee
-    builder = TransactionBuilder(Params(), 1, std::nullopt, &keystore);
+    builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root(), &keystore);
     builder.AddTransparentOutput(taddr, 5000);
     EXPECT_EQ("Change cannot be negative: -0.00006 ZEC", builder.Build().GetError());
 
@@ -368,17 +368,18 @@ TEST(TransactionBuilder, ChangeOutput)
     auto scriptPubKey = GetScriptForDestination(tkeyid);
 
     auto fakeCoin = COutPoint(uint256S("7777777777777777777777777777777777777777777777777777777777777777"), 0);
+    auto saplingAnchor = SaplingMerkleTree::empty_root();
 
     // No change address and no Sapling spends
     {
-        auto builder = TransactionBuilder(Params(), 1, std::nullopt, &keystore);
+        auto builder = TransactionBuilder(Params(), 1, std::nullopt, saplingAnchor, &keystore);
         builder.AddTransparentInput(fakeCoin, scriptPubKey, 2500);
         EXPECT_EQ("Could not determine change address", builder.Build().GetError());
     }
 
     // Change to the same address as the first Sapling spend
     {
-        auto builder = TransactionBuilder(Params(), 1, std::nullopt, &keystore);
+        auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root(), &keystore);
         builder.AddTransparentInput(fakeCoin, scriptPubKey, 2500);
         builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
         auto tx = builder.Build().GetTxOrThrow();
@@ -393,7 +394,7 @@ TEST(TransactionBuilder, ChangeOutput)
 
     // Change to a Sapling address
     {
-        auto builder = TransactionBuilder(Params(), 1, std::nullopt, &keystore);
+        auto builder = TransactionBuilder(Params(), 1, std::nullopt, saplingAnchor, &keystore);
         builder.AddTransparentInput(fakeCoin, scriptPubKey, 2500);
         builder.SendChangeTo(zChangeAddr, fvkOut.ovk);
         auto tx = builder.Build().GetTxOrThrow();
@@ -402,13 +403,13 @@ TEST(TransactionBuilder, ChangeOutput)
         EXPECT_EQ(tx.vout.size(), 0);
         EXPECT_EQ(tx.vJoinSplit.size(), 0);
         EXPECT_EQ(tx.GetSaplingSpendsCount(), 0);
-        EXPECT_EQ(tx.GetSaplingOutputsCount(), 1);
+        EXPECT_EQ(tx.GetSaplingOutputsCount(), 2);
         EXPECT_EQ(tx.GetValueBalanceSapling(), -1500);
     }
 
     // Change to a transparent address
     {
-        auto builder = TransactionBuilder(Params(), 1, std::nullopt, &keystore);
+        auto builder = TransactionBuilder(Params(), 1, std::nullopt, saplingAnchor, &keystore);
         builder.AddTransparentInput(fakeCoin, scriptPubKey, 2500);
         builder.SendChangeTo(tkeyid, {});
         auto tx = builder.Build().GetTxOrThrow();
@@ -442,7 +443,7 @@ TEST(TransactionBuilder, SetFee)
 
     // Default fee
     {
-        auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+        auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root());
         builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
         builder.AddSaplingOutput(fvk.ovk, pa, 25000, {});
         auto tx = builder.Build().GetTxOrThrow();
@@ -458,7 +459,7 @@ TEST(TransactionBuilder, SetFee)
 
     // Configured fee
     {
-        auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+        auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root());
         builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
         builder.AddSaplingOutput(fvk.ovk, pa, 25000, {});
         builder.SetFee(20000);
@@ -487,7 +488,8 @@ TEST(TransactionBuilder, CheckSaplingTxVersion)
     auto pk = sk.ToXFVK().DefaultAddress();
 
     // Cannot add Sapling outputs to a non-Sapling transaction
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+    SaplingMerkleTree tree;
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, tree.root());
     try {
         builder.AddSaplingOutput(uint256(), pk, 12345, {});
     } catch (std::runtime_error const & err) {
@@ -498,7 +500,6 @@ TEST(TransactionBuilder, CheckSaplingTxVersion)
 
     // Cannot add Sapling spends to a non-Sapling transaction
     libzcash::SaplingNote note(pk, 50000, libzcash::Zip212Enabled::BeforeZip212);
-    SaplingMerkleTree tree;
     try {
         builder.AddSaplingSpend(sk, note, tree.witness());
     } catch (std::runtime_error const & err) {
diff --git a/src/gtest/test_validation.cpp b/src/gtest/test_validation.cpp
index 64a204f12a6..b522f887e88 100644
--- a/src/gtest/test_validation.cpp
+++ b/src/gtest/test_validation.cpp
@@ -208,7 +208,7 @@ TEST(Validation, ContextualCheckInputsDetectsOldBranchId) {
 
     // Create a transparent transaction that spends the coin, targeting
     // a height during the Overwinter epoch.
-    auto builder = TransactionBuilder(params, 15, std::nullopt, &keystore);
+    auto builder = TransactionBuilder(params, 15, std::nullopt, SaplingMerkleTree::empty_root(), &keystore);
     builder.AddTransparentInput(utxo, scriptPubKey, coinValue);
     builder.AddTransparentOutput(destination, 4000);
     auto tx = builder.Build().GetTxOrThrow();
diff --git a/src/gtest/test_zip32.cpp b/src/gtest/test_zip32.cpp
index 9361ccb7286..eefdc2efe94 100644
--- a/src/gtest/test_zip32.cpp
+++ b/src/gtest/test_zip32.cpp
@@ -3,7 +3,7 @@
 
 #include <zcash/address/zip32.h>
 
-// From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/sapling_zip32.py
+// From https://github.com/zcash/zcash-test-vectors/blob/master/zcash_test_vectors/sapling/zip32.py
 // Sapling consistently uses little-endian encoding, but uint256S takes its input in
 // big-endian byte order, so the test vectors below are byte-reversed.
 TEST(ZIP32, TestVectors) {
@@ -35,102 +35,95 @@ TEST(ZIP32, TestVectors) {
         m.ToXFVK().DefaultAddress().d,
         testing::ElementsAreArray({ 0xd8, 0x62, 0x1b, 0x98, 0x1c, 0xf3, 0x00, 0xe9, 0xd4, 0xcc, 0x89 }));
 
-    auto m_1 = m.Derive(1);
-    EXPECT_EQ(m_1.depth, 1);
-    EXPECT_EQ(m_1.parentFVKTag, 0x3a71c214);
-    EXPECT_EQ(m_1.childIndex, 1);
+    auto m_1h = m.Derive(1 | HARDENED_KEY_LIMIT);
+    EXPECT_EQ(m_1h.depth, 1);
+    EXPECT_EQ(m_1h.parentFVKTag, 0x3a71c214);
+    EXPECT_EQ(m_1h.childIndex, 1 | HARDENED_KEY_LIMIT);
     EXPECT_EQ(
-        m_1.chaincode,
-        uint256S("e6bcda05678a43fad229334ef0b795a590e7c50590baf0d9b9031a690c114701"));
+        m_1h.chaincode,
+        uint256S("dbaeca68fd2ef8b45ec23ee91bd694aa2759e010c668bb3e066b20a845aacc6f"));
     EXPECT_EQ(
-        m_1.expsk.ask,
-        uint256S("0c357a2655b4b8d761794095df5cb402d3ba4a428cf6a88e7c2816a597c12b28"));
+        m_1h.expsk.ask,
+        uint256S("04bd31e1a6218db693ff0802f029043ec20f3b0b8b148cdc04be7afb2ee9f7d5"));
     EXPECT_EQ(
-        m_1.expsk.nsk,
-        uint256S("01ba6bff1018fd4eac04da7e3f2c6be9c229e662c5c4d1d6fc1ecafd8829a3e7"));
+        m_1h.expsk.nsk,
+        uint256S("0a75e557f6fcbf672e0134d4ec2d51a3f358659b4b5c46f303e6cb22687c2a37"));
     EXPECT_EQ(
-        m_1.expsk.ovk,
-        uint256S("7474a4c518551bd82f14a7f7365a8ffa403c50cfeffedf026ada8688fc81135f"));
+        m_1h.expsk.ovk,
+        uint256S("691c33ec470a1697ca37ceb237bb7f1691d2a833543514cf1f8c343319763025"));
     EXPECT_EQ(
-        m_1.dk,
-        uint256S("dcb4c170d878510e96c4a74192d7eecde9c9912b00b99a12ec91d7a232e84de0"));
+        m_1h.dk,
+        uint256S("26d53444cbe2e9929f619d810a0d05ae0deece0a72c3a7e3df9a5fd60f4088f2"));
     EXPECT_THAT(
-        m_1.ToXFVK().DefaultAddress().d,
-        testing::ElementsAreArray({ 0x8b, 0x41, 0x38, 0x32, 0x0d, 0xfa, 0xfd, 0x7b, 0x39, 0x97, 0x81 }));
+        m_1h.ToXFVK().DefaultAddress().d,
+        testing::ElementsAreArray({ 0xbc, 0xc3, 0x23, 0xe8, 0xda, 0x39, 0xb4, 0x96, 0xc0, 0x50, 0x51 }));
 
-    auto m_1_2h = m_1.Derive(2 | HARDENED_KEY_LIMIT);
-    EXPECT_EQ(m_1_2h.depth, 2);
-    EXPECT_EQ(m_1_2h.parentFVKTag, 0x079e99db);
-    EXPECT_EQ(m_1_2h.childIndex, 2 | HARDENED_KEY_LIMIT);
+    auto m_1h_2h = m_1h.Derive(2 | HARDENED_KEY_LIMIT);
+    EXPECT_EQ(m_1h_2h.depth, 2);
+    EXPECT_EQ(m_1h_2h.parentFVKTag, 0xcb238476);
+    EXPECT_EQ(m_1h_2h.childIndex, 2 | HARDENED_KEY_LIMIT);
     EXPECT_EQ(
-        m_1_2h.chaincode,
-        uint256S("35d4a883737742ca41a4baa92323bdb3c93dcb3b462a26b039971bedf415ce97"));
+        m_1h_2h.chaincode,
+        uint256S("daf7be6f80503ab34f14f236da9de2cf540ae3c100f520607980d0756c087944"));
     EXPECT_EQ(
-        m_1_2h.expsk.ask,
-        uint256S("0dc6e4fe846bda925c82e632980434e17b51dac81fc4821fa71334ee3c11e88b"));
+        m_1h_2h.expsk.ask,
+        uint256S("06512f33a6f9ae4b42fd71f9cfa08d3727522dd3089cad596fc3139eb65df37f"));
     EXPECT_EQ(
-        m_1_2h.expsk.nsk,
-        uint256S("0c99a63a275c1c66734761cfb9c62fe9bd1b953f579123d3d0e769c59d057837"));
+        m_1h_2h.expsk.nsk,
+        uint256S("00debf5999f564a3e05a0d418cf40714399a32c1bdc98ba2eb4439a0e46e9c77"));
     EXPECT_EQ(
-        m_1_2h.expsk.ovk,
-        uint256S("bc1328fc5eb693e18875c5149d06953b11d39447ebd6e38c023c22962e1881cf"));
+        m_1h_2h.expsk.ovk,
+        uint256S("ac85619305763dc29b67b75e305e5323bda7d6a530736a88417f90bf0171fcd9"));
     EXPECT_EQ(
-        m_1_2h.dk,
-        uint256S("377bb062dce7e0dcd8a0054d0ca4b4d1481b3710bfa1df12ca46ff9e9fa1eda3"));
+        m_1h_2h.dk,
+        uint256S("d148325ff6faa682558de97a9fec61dd8dc10a96d0cd214bc531e0869a9e69e4"));
     EXPECT_THAT(
-        m_1_2h.ToXFVK().DefaultAddress().d,
-        testing::ElementsAreArray({ 0xe8, 0xd0, 0x37, 0x93, 0xcd, 0xd2, 0xba, 0xcc, 0x9c, 0x70, 0x41 }));
+        m_1h_2h.ToXFVK().DefaultAddress().d,
+        testing::ElementsAreArray({ 0x98, 0x82, 0x40, 0xce, 0xa4, 0xdb, 0xc3, 0x0a, 0x73, 0x75, 0x50 }));
 
-    auto m_1_2hv = m_1_2h.ToXFVK();
+    auto m_1_2hv = m_1h_2h.ToXFVK();
     EXPECT_EQ(m_1_2hv.depth, 2);
-    EXPECT_EQ(m_1_2hv.parentFVKTag, 0x079e99db);
+    EXPECT_EQ(m_1_2hv.parentFVKTag, 0xcb238476);
     EXPECT_EQ(m_1_2hv.childIndex, 2 | HARDENED_KEY_LIMIT);
     EXPECT_EQ(
         m_1_2hv.chaincode,
-        uint256S("35d4a883737742ca41a4baa92323bdb3c93dcb3b462a26b039971bedf415ce97"));
+        uint256S("daf7be6f80503ab34f14f236da9de2cf540ae3c100f520607980d0756c087944"));
     EXPECT_EQ(
         m_1_2hv.fvk.ak,
-        uint256S("4138cffdf7200e52d4e9f4384481b4a4c4d070493a5e401e4ffa850f5a92c5a6"));
+        uint256S("4eab7275725c76ee4247ae8d941d4b53682e39da641785e097377144953f859a"));
     EXPECT_EQ(
         m_1_2hv.fvk.nk,
-        uint256S("11eee22577304f660cc036bc84b3fc88d1ec50ae8a4d657beb6b211659304e30"));
+        uint256S("be4f5d4f36018511d23a1b9a9c87af8c6dbd20212da84121c1ce884f8aa266f1"));
     EXPECT_EQ(
         m_1_2hv.fvk.ovk,
-        uint256S("bc1328fc5eb693e18875c5149d06953b11d39447ebd6e38c023c22962e1881cf"));
+        uint256S("ac85619305763dc29b67b75e305e5323bda7d6a530736a88417f90bf0171fcd9"));
     EXPECT_EQ(
         m_1_2hv.dk,
-        uint256S("377bb062dce7e0dcd8a0054d0ca4b4d1481b3710bfa1df12ca46ff9e9fa1eda3"));
-    EXPECT_EQ(m_1_2hv.DefaultAddress(), m_1_2h.ToXFVK().DefaultAddress());
+        uint256S("d148325ff6faa682558de97a9fec61dd8dc10a96d0cd214bc531e0869a9e69e4"));
+    EXPECT_EQ(m_1_2hv.DefaultAddress(), m_1h_2h.ToXFVK().DefaultAddress());
 
-    // Hardened derivation from an xfvk fails
-    EXPECT_FALSE(m_1_2hv.Derive(3 | HARDENED_KEY_LIMIT));
-
-    // Non-hardened derivation succeeds
-    auto maybe_m_1_2hv_3 = m_1_2hv.Derive(3);
-    EXPECT_TRUE(maybe_m_1_2hv_3);
-
-    auto m_1_2hv_3 = maybe_m_1_2hv_3.value();
-    EXPECT_EQ(m_1_2hv_3.depth, 3);
-    EXPECT_EQ(m_1_2hv_3.parentFVKTag, 0x7583c148);
-    EXPECT_EQ(m_1_2hv_3.childIndex, 3);
+    auto m_1h_2h_3h = m_1h_2h.Derive(3 | HARDENED_KEY_LIMIT);
+    EXPECT_EQ(m_1h_2h_3h.depth, 3);
+    EXPECT_EQ(m_1h_2h_3h.parentFVKTag, 0x2b2ddc0b);
+    EXPECT_EQ(m_1h_2h_3h.childIndex, 3 | HARDENED_KEY_LIMIT);
     EXPECT_EQ(
-        m_1_2hv_3.chaincode,
-        uint256S("e8e7d6a74a5a1c05be41baec7998d91f7b3603a4c0af495b0d43ba81cf7b938d"));
+        m_1h_2h_3h.chaincode,
+        uint256S("4bc7bd5b38fcbdb259be013bef298c8de263e4c32ccb2bcdd2ce90762d01dc33"));
     EXPECT_EQ(
-        m_1_2hv_3.fvk.ak,
-        uint256S("a3a697bdda9d648d32a97553de4754b2fac866d726d3f2c436259c507bc585b1"));
+        m_1h_2h_3h.expsk.ask,
+        uint256S("07afec4df829ace083d68145103c50692f331c4690cf52f13759e3214dd29345"));
     EXPECT_EQ(
-        m_1_2hv_3.fvk.nk,
-        uint256S("4f66c0814b769963f3bf1bc001270b50edabb27de042fc8a5607d2029e0488db"));
+        m_1h_2h_3h.expsk.nsk,
+        uint256S("0bbba7ff6efab6fbabb5b727ed3d55e40efa0de858f8c0e357503f12c27ec81a"));
     EXPECT_EQ(
-        m_1_2hv_3.fvk.ovk,
-        uint256S("f61a699934dc78441324ef628b4b4721611571e8ee3bd591eb3d4b1cfae0b969"));
+        m_1h_2h_3h.expsk.ovk,
+        uint256S("e39e4b1b9c5c1b31988beffb515522a95de718afa880e36c9d2ebef20cea361e"));
     EXPECT_EQ(
-        m_1_2hv_3.dk,
-        uint256S("6ee53b1261f2c9c0f7359ab236f87b52a0f1b0ce43305cdad92ebb63c350cbbe"));
+        m_1h_2h_3h.dk,
+        uint256S("6abb7b109c7270edaa3a36dc140d3f70bf8cd271b69d606f5aadf3a4596cfc57"));
     EXPECT_THAT(
-        m_1_2hv_3.DefaultAddress().d,
-        testing::ElementsAreArray({ 0x03, 0x0f, 0xfb, 0x26, 0x3a, 0x93, 0x9e, 0x23, 0x0e, 0x96, 0xdd }));
+        m_1h_2h_3h.ToXFVK().DefaultAddress().d,
+        testing::ElementsAreArray({ 0x5a, 0x75, 0xbe, 0x14, 0x00, 0x53, 0x0b, 0x4b, 0x7a, 0xdd, 0x52 }));
 }
 
 TEST(ZIP32, ParseHDKeypathAccount) {
diff --git a/src/gtest/utils.cpp b/src/gtest/utils.cpp
index dee99399c83..f4342af1bf8 100644
--- a/src/gtest/utils.cpp
+++ b/src/gtest/utils.cpp
@@ -79,9 +79,15 @@ template<> void AppendRandomLeaf(OrchardMerkleFrontier &tree) {
     // fortunately the tests only require that the tree root change.
     // TODO: Remove the need to create proofs by having a testing-only way to
     // append a random leaf to OrchardMerkleFrontier.
+    RawHDSeed seed(32, 0);
+    auto to = libzcash::OrchardSpendingKey::ForAccount(seed, 133, 0)
+        .ToFullViewingKey()
+        .GetChangeAddress();
     uint256 orchardAnchor;
     uint256 dataToBeSigned;
-    auto builder = orchard::Builder(true, true, orchardAnchor);
+    // TODO: Create bundle.
+    auto builder = orchard::Builder(false, orchardAnchor);
+    builder.AddOutput(std::nullopt, to, 0, std::nullopt);
     auto bundle = builder.Build().value().ProveAndSign({}, dataToBeSigned).value();
     tree.AppendBundle(bundle);
 }
diff --git a/src/rust/src/test_harness_ffi.rs b/src/rust/src/test_harness_ffi.rs
index 2d22d741e20..078ec4cf84f 100644
--- a/src/rust/src/test_harness_ffi.rs
+++ b/src/rust/src/test_harness_ffi.rs
@@ -1,10 +1,10 @@
+use std::convert::TryFrom;
+
 use group::{ff::Field, Group, GroupEncoding};
 use rand::{thread_rng, Rng};
+use sapling::value::ValueCommitment;
 use zcash_note_encryption::EphemeralKeyBytes;
-use zcash_primitives::{
-    sapling::{self, redjubjub, value::ValueCommitment},
-    transaction::components::{sapling as sapling_tx, Amount},
-};
+use zcash_primitives::transaction::components::Amount;
 
 pub(crate) fn test_only_invalid_sapling_bundle(
     spends: usize,
@@ -27,14 +27,17 @@ pub(crate) fn test_only_invalid_sapling_bundle(
             .unwrap();
             let anchor = jubjub::Base::random(&mut rng);
             let nullifier = sapling::Nullifier(rng.gen());
-            let rk = redjubjub::PublicKey(jubjub::ExtendedPoint::random(&mut rng));
+            let rk = redjubjub::VerificationKey::try_from(
+                jubjub::ExtendedPoint::random(&mut rng).to_bytes(),
+            )
+            .unwrap();
             let zkproof = gen_array(&mut rng);
             let spend_auth_sig = {
                 let tmp = gen_array::<_, 64>(&mut rng);
-                redjubjub::Signature::read(&tmp[..]).unwrap()
+                redjubjub::Signature::from(tmp)
             };
 
-            sapling_tx::SpendDescription::temporary_zcashd_from_parts(
+            sapling::bundle::SpendDescription::from_parts(
                 cv,
                 anchor,
                 nullifier,
@@ -61,7 +64,7 @@ pub(crate) fn test_only_invalid_sapling_bundle(
             let out_ciphertext = gen_array(&mut rng);
             let zkproof = gen_array(&mut rng);
 
-            sapling_tx::OutputDescription::temporary_zcashd_from_parts(
+            sapling::bundle::OutputDescription::from_parts(
                 cv,
                 cmu,
                 ephemeral_key,
@@ -74,16 +77,16 @@ pub(crate) fn test_only_invalid_sapling_bundle(
 
     let binding_sig = {
         let tmp = gen_array::<_, 64>(&mut rng);
-        redjubjub::Signature::read(&tmp[..]).unwrap()
+        redjubjub::Signature::from(tmp)
     };
 
-    let bundle = sapling_tx::Bundle::temporary_zcashd_from_parts(
+    let bundle = sapling::Bundle::from_parts(
         spends,
         outputs,
         Amount::from_i64(value_balance).unwrap(),
-        sapling_tx::Authorized { binding_sig },
+        sapling::bundle::Authorized { binding_sig },
     );
-    Box::new(crate::sapling::Bundle(Some(bundle)))
+    Box::new(crate::sapling::Bundle(bundle))
 }
 
 pub(crate) fn test_only_replace_sapling_nullifier(
@@ -92,18 +95,18 @@ pub(crate) fn test_only_replace_sapling_nullifier(
     nullifier: [u8; 32],
 ) {
     if let Some(bundle) = bundle.0.as_mut() {
-        *bundle = sapling_tx::Bundle::temporary_zcashd_from_parts(
+        *bundle = sapling::Bundle::from_parts(
             bundle
                 .shielded_spends()
                 .iter()
                 .enumerate()
                 .map(|(i, spend)| {
                     if i == spend_index {
-                        sapling_tx::SpendDescription::temporary_zcashd_from_parts(
+                        sapling::bundle::SpendDescription::from_parts(
                             spend.cv().clone(),
                             *spend.anchor(),
                             sapling::Nullifier(nullifier),
-                            spend.rk().clone(),
+                            *spend.rk(),
                             *spend.zkproof(),
                             *spend.spend_auth_sig(),
                         )
@@ -116,6 +119,7 @@ pub(crate) fn test_only_replace_sapling_nullifier(
             *bundle.value_balance(),
             *bundle.authorization(),
         )
+        .expect("Prior bundle was valid");
     }
 }
 
@@ -127,7 +131,7 @@ pub(crate) fn test_only_replace_sapling_output_parts(
     out_ciphertext: [u8; 80],
 ) {
     if let Some(bundle) = bundle.0.as_mut() {
-        *bundle = sapling_tx::Bundle::temporary_zcashd_from_parts(
+        *bundle = sapling::Bundle::from_parts(
             bundle.shielded_spends().to_vec(),
             bundle
                 .shielded_outputs()
@@ -135,7 +139,7 @@ pub(crate) fn test_only_replace_sapling_output_parts(
                 .enumerate()
                 .map(|(i, output)| {
                     if i == output_index {
-                        sapling_tx::OutputDescription::temporary_zcashd_from_parts(
+                        sapling::bundle::OutputDescription::from_parts(
                             output.cv().clone(),
                             sapling::note::ExtractedNoteCommitment::from_bytes(&cmu).unwrap(),
                             output.ephemeral_key().clone(),
@@ -151,5 +155,6 @@ pub(crate) fn test_only_replace_sapling_output_parts(
             *bundle.value_balance(),
             *bundle.authorization(),
         )
+        .expect("Prior bundle was valid");
     }
 }
diff --git a/src/rust/src/tests/key_components.rs b/src/rust/src/tests/key_components.rs
index f268bb1d918..8cc8d8f7b80 100644
--- a/src/rust/src/tests/key_components.rs
+++ b/src/rust/src/tests/key_components.rs
@@ -1,7 +1,7 @@
 use group::GroupEncoding;
-use zcash_primitives::{
-    constants::SPENDING_KEY_GENERATOR,
-    sapling::{Diversifier, Nullifier, ProofGenerationKey, Rseed},
+use sapling::{
+    constants::SPENDING_KEY_GENERATOR, keys::SpendValidatingKey, value::NoteValue, Diversifier,
+    Nullifier, ProofGenerationKey, Rseed,
 };
 
 use crate::sapling::spec::{ask_to_ak, check_diversifier, crh_ivk, ivk_to_pkd, nsk_to_nk};
@@ -661,7 +661,10 @@ fn key_components() {
             assert_eq!(&ak, &tv.ak);
         }
 
-        let pgk = ProofGenerationKey { ak, nsk };
+        let pgk = ProofGenerationKey {
+            ak: SpendValidatingKey::temporary_zcash_from_bytes(&ak.to_bytes()).unwrap(),
+            nsk,
+        };
         let fvk = pgk.to_viewing_key();
         assert_eq!(&fvk.nk.0.to_bytes(), &tv.nk);
         {
@@ -686,7 +689,7 @@ fn key_components() {
         }
 
         let note_r = jubjub::Scalar::from_bytes(&tv.note_r).unwrap();
-        let note = addr.create_note(tv.note_v, Rseed::BeforeZip212(note_r));
+        let note = addr.create_note(NoteValue::from_raw(tv.note_v), Rseed::BeforeZip212(note_r));
         assert_eq!(&note.cmu().to_bytes(), &tv.note_cm);
 
         assert_eq!(note.nf(&fvk.nk, tv.note_pos), Nullifier(tv.note_nf));
diff --git a/src/rust/src/tests/mod.rs b/src/rust/src/tests/mod.rs
index 942f02082d2..b09a0856598 100644
--- a/src/rust/src/tests/mod.rs
+++ b/src/rust/src/tests/mod.rs
@@ -1,5 +1,5 @@
 use group::GroupEncoding;
-use zcash_primitives::constants::{
+use sapling::constants::{
     NOTE_COMMITMENT_RANDOMNESS_GENERATOR, NULLIFIER_POSITION_GENERATOR,
     PROOF_GENERATION_KEY_GENERATOR, SPENDING_KEY_GENERATOR, VALUE_COMMITMENT_RANDOMNESS_GENERATOR,
     VALUE_COMMITMENT_VALUE_GENERATOR,
diff --git a/src/rust/src/tests/signatures.rs b/src/rust/src/tests/signatures.rs
index 028e3a01614..31d01ab90c5 100644
--- a/src/rust/src/tests/signatures.rs
+++ b/src/rust/src/tests/signatures.rs
@@ -1,7 +1,4 @@
-use zcash_primitives::{
-    constants::SPENDING_KEY_GENERATOR,
-    sapling::redjubjub::{PrivateKey, PublicKey, Signature},
-};
+use std::convert::TryFrom;
 
 #[test]
 fn redjubjub_signatures() {
@@ -16,7 +13,7 @@ fn redjubjub_signatures() {
         rsig: [u8; 64],
     }
 
-    // From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/sapling_signatures.py
+    // From https://github.com/zcash/zcash-test-vectors/blob/master/zcash_test_vectors/sapling/redjubjub.py
     let test_vectors = vec![
         TestVector {
             sk: [
@@ -50,18 +47,18 @@ fn redjubjub_signatures() {
                 0x00, 0x00, 0x00, 0x00,
             ],
             sig: [
-                0xea, 0xa0, 0x57, 0x47, 0x6b, 0x4a, 0xb4, 0x82, 0x28, 0x8b, 0x93, 0xdf, 0x8f, 0xe0,
-                0xc5, 0xce, 0x9d, 0x78, 0x83, 0x67, 0xf2, 0xbe, 0x55, 0x1b, 0x7f, 0x7a, 0x82, 0xa6,
-                0xdb, 0x36, 0x04, 0x68, 0xde, 0xb9, 0xa7, 0xb7, 0xaf, 0xaa, 0xdf, 0xec, 0xa6, 0xf4,
-                0x81, 0x19, 0x3d, 0xc6, 0x57, 0x57, 0x47, 0xf6, 0x0a, 0x1a, 0x8a, 0x48, 0xff, 0x0a,
-                0xd7, 0x0c, 0xf8, 0xcb, 0x8d, 0x52, 0x8e, 0x08,
+                0xdc, 0xa3, 0xbb, 0x2c, 0xb8, 0xf0, 0x48, 0xcc, 0xab, 0x10, 0xae, 0xd7, 0x75, 0x46,
+                0xc1, 0xdb, 0xb1, 0x0c, 0xc4, 0xfb, 0x15, 0xab, 0x02, 0xac, 0xae, 0xf9, 0x44, 0xdd,
+                0xab, 0x8b, 0x67, 0x22, 0x54, 0x5f, 0xda, 0x4c, 0x62, 0x04, 0x6d, 0x69, 0xd9, 0x8f,
+                0x92, 0x2f, 0x4e, 0x8c, 0x21, 0x0b, 0xc4, 0x7b, 0x4f, 0xdd, 0xe0, 0xa1, 0x94, 0x71,
+                0x79, 0x80, 0x4c, 0x1a, 0xce, 0x56, 0x90, 0x05,
             ],
             rsig: [
-                0xd5, 0x6f, 0x0d, 0x91, 0xaf, 0x42, 0x4e, 0x1f, 0x1c, 0x7f, 0xb8, 0x6b, 0xa4, 0xee,
-                0xd1, 0x43, 0xcc, 0x16, 0x66, 0x0c, 0x5f, 0xe8, 0xd7, 0xdc, 0x0d, 0x28, 0x4b, 0xcf,
-                0x65, 0xa0, 0x89, 0xe9, 0x8b, 0x56, 0x1f, 0x9f, 0x20, 0x1a, 0x63, 0x3d, 0x70, 0x0c,
-                0xd3, 0x98, 0x1e, 0x8c, 0xac, 0x07, 0xb5, 0xa8, 0x7e, 0xfa, 0x61, 0x86, 0x06, 0x2d,
-                0xd8, 0xe5, 0xd6, 0x32, 0x5e, 0x7b, 0x82, 0x02,
+                0x70, 0xc2, 0x84, 0x50, 0x4e, 0x90, 0xf0, 0x00, 0x8e, 0x8e, 0xd2, 0x20, 0x8f, 0x49,
+                0x69, 0x72, 0x7a, 0x41, 0x5e, 0xc3, 0x10, 0x2c, 0x29, 0x9e, 0x39, 0x8b, 0x6c, 0x16,
+                0x57, 0x2b, 0xd9, 0x64, 0x3e, 0xe1, 0x01, 0x17, 0x66, 0x68, 0x1e, 0x40, 0x6e, 0xe6,
+                0xbe, 0xe3, 0xd0, 0x3e, 0xe8, 0xf2, 0x71, 0x76, 0xe3, 0x2f, 0xba, 0xbd, 0xde, 0xd2,
+                0x0b, 0x0d, 0x17, 0x86, 0xa4, 0xee, 0x18, 0x01,
             ],
         },
         TestVector {
@@ -96,18 +93,18 @@ fn redjubjub_signatures() {
                 0x01, 0x01, 0x01, 0x01,
             ],
             sig: [
-                0x22, 0x35, 0x54, 0x94, 0xa8, 0x31, 0x6a, 0xb1, 0x34, 0x73, 0xf5, 0x5e, 0x62, 0x66,
-                0xb2, 0xfb, 0x41, 0x97, 0x31, 0x5e, 0xac, 0x62, 0xf8, 0x2c, 0xc7, 0x3d, 0xca, 0xca,
-                0x19, 0x90, 0x90, 0xf1, 0x5b, 0xe1, 0x98, 0xce, 0x7d, 0x3f, 0x9f, 0xc8, 0xff, 0xf5,
-                0x50, 0xe1, 0x08, 0x81, 0xec, 0x49, 0xff, 0x27, 0x36, 0x9e, 0x7d, 0x4f, 0xd9, 0x64,
-                0x01, 0x53, 0x49, 0x2a, 0x0a, 0x06, 0x25, 0x08,
+                0xb5, 0xa1, 0xf3, 0x2d, 0x3d, 0x50, 0xfc, 0x73, 0x8b, 0x5c, 0x3b, 0x4e, 0x99, 0x60,
+                0x72, 0x9c, 0xe4, 0x31, 0x6b, 0xa7, 0x72, 0x1a, 0x12, 0x68, 0x66, 0x04, 0xfe, 0xba,
+                0x6b, 0xd7, 0x48, 0x45, 0x00, 0x70, 0xcb, 0x92, 0x24, 0x06, 0xfd, 0xfc, 0x5d, 0x60,
+                0xde, 0xa9, 0xbe, 0x3a, 0x52, 0x6a, 0x16, 0xcf, 0xeb, 0x87, 0x77, 0x79, 0xfb, 0x78,
+                0x2d, 0x5d, 0x41, 0x39, 0x5b, 0x45, 0x5f, 0x04,
             ],
             rsig: [
-                0xf4, 0xb8, 0x94, 0xba, 0x84, 0xce, 0x1e, 0xc3, 0x8a, 0x63, 0x15, 0x2f, 0xc4, 0x09,
-                0xf9, 0x47, 0xd6, 0x1a, 0xbb, 0x1f, 0x48, 0x91, 0x63, 0x6b, 0xc3, 0xee, 0x19, 0xef,
-                0x6d, 0x4b, 0x30, 0xc0, 0xfd, 0x22, 0x86, 0x6b, 0x84, 0xff, 0xbc, 0x7e, 0x2a, 0x78,
-                0xc4, 0x3f, 0x57, 0x83, 0xd2, 0xd2, 0xea, 0xd0, 0x78, 0x59, 0x55, 0x03, 0x74, 0x43,
-                0xc2, 0xf4, 0xd5, 0x2f, 0x78, 0x5e, 0xee, 0x07,
+                0x5a, 0x5a, 0x20, 0xd2, 0x00, 0xef, 0xdd, 0xd4, 0x98, 0xdf, 0xae, 0x2a, 0x9e, 0xf8,
+                0xcf, 0x01, 0x28, 0x1a, 0x89, 0x19, 0x01, 0x8a, 0x82, 0x4c, 0xc7, 0xa4, 0x98, 0x3b,
+                0x9a, 0x0d, 0x4a, 0x06, 0xff, 0x17, 0x20, 0x79, 0xe0, 0x13, 0xd4, 0x2a, 0x2a, 0x3a,
+                0x88, 0xa6, 0x52, 0x0c, 0x86, 0xfc, 0xe3, 0xb9, 0x8e, 0x1e, 0xfa, 0xa3, 0x25, 0x83,
+                0x2a, 0x6a, 0x56, 0x58, 0xd8, 0xdd, 0x7c, 0x0a,
             ],
         },
         TestVector {
@@ -142,18 +139,18 @@ fn redjubjub_signatures() {
                 0x02, 0x02, 0x02, 0x02,
             ],
             sig: [
-                0xdd, 0x65, 0x21, 0x01, 0x4d, 0xff, 0x70, 0x6e, 0x3a, 0x38, 0x52, 0x7a, 0x86, 0xb6,
-                0xc1, 0x6e, 0x94, 0x14, 0x80, 0xe7, 0x33, 0xef, 0xf7, 0x9e, 0xbe, 0x0c, 0x43, 0x03,
-                0x79, 0xd7, 0x57, 0x04, 0x9d, 0xb7, 0x90, 0xcd, 0x5e, 0x14, 0x44, 0x7c, 0x38, 0x6f,
-                0x5f, 0xcb, 0x41, 0x9f, 0x27, 0xc4, 0x41, 0x3f, 0x35, 0x88, 0xfa, 0x21, 0x42, 0xd2,
-                0xcf, 0xba, 0xed, 0x08, 0x2c, 0xc6, 0xdb, 0x07,
+                0x1f, 0x3e, 0x8a, 0x94, 0x31, 0x0c, 0x20, 0x71, 0xa7, 0x0f, 0x9d, 0xf5, 0xe7, 0x9a,
+                0xa9, 0xe8, 0x48, 0x5d, 0xec, 0xcb, 0x17, 0x8b, 0xdf, 0xf9, 0x80, 0x5f, 0xcb, 0xe6,
+                0xf7, 0xd5, 0x51, 0xee, 0xe3, 0xc3, 0x54, 0x2c, 0xa7, 0x5c, 0x9d, 0x8d, 0x4a, 0xdc,
+                0x54, 0xd7, 0x2c, 0x3d, 0xbe, 0x28, 0x62, 0x6d, 0x20, 0x78, 0x5b, 0xb7, 0xf5, 0x88,
+                0xc1, 0xa5, 0x82, 0xb8, 0x93, 0xdb, 0xb6, 0x01,
             ],
             rsig: [
-                0xd8, 0x94, 0x45, 0xcb, 0x9b, 0xd1, 0x03, 0x35, 0x69, 0x23, 0x1d, 0xd6, 0x28, 0xaa,
-                0x62, 0x81, 0x09, 0xfe, 0x93, 0x50, 0x2b, 0xf2, 0x2f, 0x9a, 0x5f, 0x37, 0xb1, 0x4e,
-                0x51, 0x7f, 0x9a, 0x20, 0x54, 0xae, 0xe3, 0xc8, 0x1b, 0x60, 0xb3, 0xf0, 0x55, 0x1e,
-                0x32, 0xf7, 0x93, 0x5a, 0xbc, 0x2f, 0x37, 0xb9, 0x9a, 0xb3, 0xec, 0x99, 0x68, 0x02,
-                0xef, 0xd6, 0x50, 0x69, 0xe1, 0x28, 0x12, 0x08,
+                0xd1, 0x36, 0x21, 0x4c, 0x5d, 0x52, 0x8e, 0xa3, 0xd4, 0xcb, 0x7b, 0x63, 0x1a, 0x6b,
+                0xb0, 0x36, 0x06, 0x49, 0x73, 0xa1, 0x08, 0xb7, 0x33, 0xa5, 0xe3, 0xa4, 0x52, 0xab,
+                0x52, 0xa6, 0x59, 0xe5, 0x67, 0xcb, 0x55, 0xd2, 0x64, 0x4e, 0x74, 0xb6, 0xe8, 0x42,
+                0x6f, 0x2a, 0x7d, 0xd2, 0xa0, 0x4d, 0x2d, 0xda, 0x49, 0x35, 0xcc, 0x38, 0x20, 0xb7,
+                0x7a, 0x9c, 0x1a, 0xb6, 0x19, 0x86, 0x3c, 0x05,
             ],
         },
         TestVector {
@@ -188,18 +185,18 @@ fn redjubjub_signatures() {
                 0x03, 0x03, 0x03, 0x03,
             ],
             sig: [
-                0x72, 0x79, 0xa7, 0x5c, 0x01, 0x36, 0x75, 0xb3, 0x29, 0x84, 0xe5, 0xc7, 0x3a, 0x98,
-                0x91, 0xeb, 0xf0, 0xb2, 0x29, 0xb1, 0x6e, 0x62, 0x35, 0xba, 0x36, 0xdf, 0xa1, 0xb5,
-                0xa1, 0x0c, 0x5e, 0x44, 0x57, 0x81, 0x91, 0x89, 0x7c, 0x06, 0xb8, 0x52, 0x4a, 0x26,
-                0x74, 0xaa, 0x7a, 0x0c, 0x8c, 0x23, 0x5f, 0x52, 0xd3, 0x3a, 0xc9, 0x2c, 0x70, 0x56,
-                0xb2, 0xbe, 0x95, 0x3c, 0x3f, 0xaa, 0x3d, 0x07,
+                0x12, 0xc7, 0x8d, 0xdd, 0x20, 0xd3, 0x0a, 0x61, 0xf8, 0x93, 0x0c, 0x6f, 0xe0, 0x85,
+                0x0f, 0xd1, 0x12, 0xbb, 0x7b, 0xe8, 0x8b, 0x12, 0x38, 0xea, 0x33, 0xd6, 0xbe, 0xf8,
+                0x81, 0xc1, 0x02, 0xd1, 0x04, 0xaa, 0x36, 0x54, 0x4a, 0x78, 0x47, 0x1c, 0x9e, 0x28,
+                0x42, 0xe6, 0xfd, 0x42, 0x55, 0x83, 0x46, 0xcf, 0xf4, 0x31, 0x27, 0x03, 0x26, 0x66,
+                0xeb, 0x11, 0x6f, 0x44, 0x2a, 0x28, 0x48, 0x0c,
             ],
             rsig: [
-                0xaa, 0xd4, 0x82, 0x8c, 0xb3, 0x42, 0xcf, 0x09, 0xb0, 0x0e, 0x30, 0x2c, 0xbb, 0xe7,
-                0xcc, 0x3e, 0x95, 0xfe, 0x1f, 0xf8, 0x28, 0x74, 0x8e, 0x5f, 0x5b, 0xc6, 0x9c, 0xbf,
-                0xde, 0x6e, 0x27, 0x22, 0xd7, 0x64, 0x35, 0x68, 0x7e, 0x85, 0x0c, 0xd3, 0x07, 0xa9,
-                0xc1, 0x82, 0xec, 0x10, 0xe6, 0x88, 0x1d, 0xd6, 0x5e, 0xed, 0xc1, 0x1f, 0xa7, 0xb4,
-                0x6d, 0xe3, 0xa7, 0x19, 0x59, 0xce, 0xc0, 0x02,
+                0x01, 0xba, 0xaa, 0x26, 0x27, 0x4c, 0x14, 0x9a, 0xcf, 0x12, 0xe1, 0xcc, 0xf5, 0x50,
+                0x7d, 0x56, 0x79, 0x04, 0x82, 0xf0, 0x67, 0xe5, 0xc9, 0x2b, 0x32, 0x19, 0xad, 0x6b,
+                0xf9, 0x11, 0x18, 0xcc, 0x3f, 0xce, 0x8d, 0x2a, 0x23, 0x19, 0x8a, 0x3b, 0x29, 0x0a,
+                0x7b, 0xf6, 0x8c, 0x2a, 0xc0, 0x7b, 0x5d, 0x90, 0x62, 0xb9, 0xf8, 0x68, 0x66, 0x2b,
+                0xb2, 0x52, 0x49, 0x12, 0xd4, 0x85, 0x6e, 0x0c,
             ],
         },
         TestVector {
@@ -234,18 +231,18 @@ fn redjubjub_signatures() {
                 0x04, 0x04, 0x04, 0x04,
             ],
             sig: [
-                0x51, 0x23, 0xb3, 0x1f, 0x84, 0xaf, 0x0c, 0x35, 0x5e, 0x13, 0xe7, 0x8a, 0x64, 0xd7,
-                0xa3, 0xcd, 0xfd, 0x6b, 0xdf, 0xfd, 0xc7, 0x33, 0x38, 0xd9, 0x31, 0x7f, 0x73, 0x43,
-                0x91, 0xa5, 0x5a, 0xe6, 0x25, 0x8f, 0x69, 0x80, 0xb9, 0xc7, 0xd1, 0x90, 0xcf, 0xa3,
-                0x65, 0x81, 0xa9, 0xa4, 0x7a, 0x86, 0x3f, 0xd3, 0xbf, 0x76, 0x59, 0x42, 0x22, 0x95,
-                0xb7, 0x5f, 0xd1, 0x22, 0xc3, 0xdd, 0x8a, 0x05,
+                0x77, 0x4a, 0xc4, 0x67, 0x3f, 0x09, 0xf3, 0xac, 0x57, 0x89, 0xb2, 0x86, 0xb5, 0xee,
+                0xcb, 0xed, 0xb2, 0x57, 0x23, 0x4e, 0x8c, 0xdf, 0xd9, 0x3f, 0x02, 0x89, 0x09, 0x78,
+                0xa6, 0xbb, 0xa6, 0x11, 0x69, 0xed, 0x48, 0xf9, 0xe1, 0xc9, 0xfd, 0x13, 0x19, 0xbd,
+                0x33, 0x0d, 0x2c, 0xf5, 0xb4, 0x91, 0x01, 0x0d, 0x69, 0xb0, 0x43, 0xf4, 0x64, 0x8b,
+                0xff, 0x55, 0x41, 0x62, 0xc6, 0xa6, 0xdc, 0x09,
             ],
             rsig: [
-                0x5b, 0xae, 0x25, 0x4f, 0xbd, 0xed, 0x60, 0x7a, 0x5c, 0x48, 0xb5, 0x30, 0x29, 0xf5,
-                0x9b, 0xa7, 0x06, 0x32, 0x48, 0x79, 0xaa, 0x18, 0xd9, 0xc4, 0x73, 0x19, 0x00, 0x4b,
-                0xe0, 0x2c, 0xec, 0xe0, 0xb8, 0xbb, 0x02, 0x4a, 0x7a, 0xab, 0xaa, 0x0a, 0x64, 0x0f,
-                0x3a, 0x54, 0xdc, 0xda, 0xf2, 0x11, 0x31, 0x46, 0x9a, 0x50, 0x06, 0xbe, 0x27, 0x81,
-                0xa5, 0x67, 0xff, 0xa6, 0x50, 0x3a, 0x35, 0x03,
+                0x7c, 0x6c, 0x49, 0x8d, 0xe0, 0x01, 0x78, 0x61, 0x09, 0xb3, 0x03, 0xa4, 0xc5, 0xdc,
+                0xb7, 0xfd, 0x07, 0x57, 0x50, 0xa0, 0xb9, 0xdf, 0x5e, 0x1e, 0x2a, 0x8e, 0x75, 0x47,
+                0xb7, 0xed, 0x70, 0xcc, 0x0b, 0x56, 0xa5, 0xbf, 0xa9, 0x65, 0x78, 0x43, 0xef, 0xd8,
+                0x9c, 0x66, 0xa8, 0x4f, 0x41, 0xd2, 0xb1, 0xb5, 0x07, 0x51, 0x19, 0x6b, 0x1e, 0x8c,
+                0x0c, 0x44, 0x98, 0x60, 0x06, 0x96, 0xa4, 0x04,
             ],
         },
         TestVector {
@@ -280,18 +277,18 @@ fn redjubjub_signatures() {
                 0x05, 0x05, 0x05, 0x05,
             ],
             sig: [
-                0xdc, 0x18, 0xc8, 0x8d, 0x96, 0x44, 0x42, 0x40, 0x6d, 0x65, 0x0a, 0xa2, 0xff, 0xbd,
-                0x83, 0xd1, 0x13, 0xbf, 0x6a, 0x19, 0xda, 0x78, 0xf2, 0x66, 0x5b, 0x29, 0x4f, 0xa5,
-                0xfa, 0x45, 0x0b, 0x92, 0x81, 0xa0, 0x7e, 0x32, 0x0c, 0x1a, 0xa3, 0x1d, 0x32, 0x44,
-                0x9e, 0x00, 0xc5, 0xc3, 0x2d, 0xb2, 0xf4, 0x13, 0xdf, 0x0b, 0x63, 0xd0, 0x72, 0x8f,
-                0xa4, 0x09, 0x41, 0xa8, 0xda, 0x02, 0x4f, 0x01,
+                0x9a, 0x25, 0x42, 0x9f, 0x3e, 0xfd, 0x9b, 0x2f, 0x7d, 0xe2, 0x9e, 0x45, 0x12, 0x8d,
+                0xd7, 0xb7, 0x60, 0xf0, 0x50, 0x8c, 0xd9, 0x58, 0x21, 0x82, 0xab, 0xaf, 0x53, 0xdd,
+                0x76, 0xc0, 0x34, 0x2c, 0xe4, 0x1b, 0x4a, 0xcf, 0x8e, 0x0a, 0x48, 0x24, 0xe4, 0x11,
+                0x08, 0xc2, 0x02, 0x65, 0x73, 0x11, 0x4b, 0x60, 0xbe, 0xec, 0xb1, 0x74, 0x01, 0x2a,
+                0x2b, 0xdb, 0xee, 0xcb, 0xaa, 0x00, 0xb5, 0x06,
             ],
             rsig: [
-                0x59, 0xe2, 0xe8, 0x18, 0x76, 0x6c, 0x50, 0xfc, 0x8f, 0x38, 0x40, 0xb2, 0x72, 0xaf,
-                0x9a, 0xd9, 0x47, 0x56, 0xc8, 0x41, 0x32, 0x95, 0xfc, 0x79, 0x5f, 0xaf, 0xbc, 0xc0,
-                0x71, 0x8e, 0x6c, 0x08, 0x16, 0x9a, 0x00, 0xd5, 0x83, 0x02, 0x77, 0x2a, 0x28, 0x28,
-                0x43, 0xe8, 0x88, 0xd9, 0x81, 0xfa, 0x04, 0x79, 0x5d, 0x01, 0x4c, 0xf9, 0xc8, 0xcd,
-                0xb9, 0x07, 0xff, 0x1b, 0x43, 0x0d, 0x92, 0x00,
+                0xcf, 0xf5, 0x83, 0x57, 0x13, 0xbe, 0x07, 0xfb, 0xe1, 0x25, 0xbb, 0xf2, 0x7a, 0x63,
+                0x6a, 0xdd, 0x13, 0x1c, 0x90, 0x81, 0x71, 0x6c, 0x52, 0xfd, 0xa8, 0x75, 0x42, 0x6d,
+                0x03, 0x98, 0x2c, 0xd2, 0x7e, 0xbd, 0x14, 0xb4, 0x22, 0x7b, 0x83, 0x96, 0x15, 0xfd,
+                0x03, 0x71, 0xbf, 0xdb, 0x8a, 0x30, 0xab, 0xdd, 0xff, 0x74, 0xd7, 0x95, 0xf3, 0xe2,
+                0x7d, 0x1d, 0x47, 0xc6, 0x29, 0x46, 0x9b, 0x08,
             ],
         },
         TestVector {
@@ -326,18 +323,18 @@ fn redjubjub_signatures() {
                 0x06, 0x06, 0x06, 0x06,
             ],
             sig: [
-                0x9a, 0xf6, 0xf2, 0x80, 0x0f, 0x4b, 0x80, 0xf7, 0x93, 0xbe, 0x64, 0x8a, 0x43, 0x9f,
-                0x86, 0xe5, 0x7d, 0xa1, 0xb9, 0x19, 0x99, 0x9e, 0x41, 0x91, 0x09, 0x99, 0xd4, 0x2e,
-                0xd0, 0xf3, 0x89, 0x6d, 0xb7, 0x6e, 0x06, 0x38, 0x8b, 0x27, 0x2c, 0x99, 0x85, 0x8b,
-                0x55, 0x04, 0xd0, 0x2e, 0xc6, 0xb4, 0xd5, 0x25, 0xb8, 0x71, 0x38, 0x10, 0x50, 0x5f,
-                0x4f, 0xc0, 0x31, 0x08, 0x3a, 0x14, 0xbf, 0x09,
+                0xbb, 0xe0, 0x23, 0x59, 0x87, 0xc6, 0xe0, 0xec, 0x68, 0x6d, 0xdb, 0x8a, 0x65, 0x72,
+                0x66, 0xad, 0x60, 0x5f, 0x7b, 0x75, 0x95, 0x5b, 0xb0, 0xe8, 0x02, 0xf8, 0x81, 0x64,
+                0xa0, 0xff, 0xe1, 0x0c, 0x3b, 0x73, 0x85, 0x04, 0xab, 0xb3, 0xd1, 0x05, 0x62, 0xb9,
+                0x27, 0xb3, 0xd2, 0x9f, 0xe9, 0xb0, 0xd3, 0x56, 0x28, 0x6a, 0xea, 0xe5, 0xa2, 0xac,
+                0x9e, 0x43, 0x5f, 0x20, 0x79, 0x1a, 0xf8, 0x00,
             ],
             rsig: [
-                0x3f, 0x7d, 0x50, 0x71, 0xb8, 0x76, 0x17, 0x49, 0x05, 0x71, 0xa8, 0xbe, 0x91, 0x74,
-                0x9e, 0x69, 0xf6, 0xbc, 0xba, 0x5a, 0xb6, 0x26, 0xe4, 0x2f, 0xf9, 0x2d, 0x0d, 0x7d,
-                0xab, 0x73, 0xf3, 0x03, 0x61, 0xe5, 0xa2, 0x24, 0x99, 0x8e, 0x1f, 0x5e, 0xa1, 0xe5,
-                0xf8, 0x68, 0x9a, 0x06, 0xa2, 0x77, 0x48, 0xbf, 0x74, 0x19, 0x63, 0xef, 0x51, 0x33,
-                0x22, 0xf4, 0xa1, 0xba, 0x99, 0xaa, 0x36, 0x03,
+                0x6d, 0xe3, 0x2b, 0x54, 0x15, 0xd7, 0x7a, 0x90, 0x5f, 0x09, 0x03, 0x90, 0x2a, 0x11,
+                0x7e, 0xda, 0x79, 0x3c, 0x70, 0x8e, 0x23, 0xa5, 0x42, 0x45, 0xba, 0x8a, 0x8d, 0x1f,
+                0xe0, 0x26, 0x75, 0x23, 0x23, 0x15, 0x65, 0xe0, 0x57, 0x09, 0xae, 0xd9, 0x6c, 0x22,
+                0x1f, 0xb1, 0xf3, 0xd0, 0x42, 0x04, 0x35, 0x03, 0xff, 0x33, 0x85, 0x85, 0xa9, 0xbb,
+                0x98, 0x9c, 0x9d, 0xd4, 0x30, 0xd6, 0xd6, 0x0b,
             ],
         },
         TestVector {
@@ -372,18 +369,18 @@ fn redjubjub_signatures() {
                 0x07, 0x07, 0x07, 0x07,
             ],
             sig: [
-                0x64, 0x59, 0x67, 0x6a, 0x94, 0x16, 0x34, 0xec, 0xb6, 0x1e, 0x59, 0xb7, 0x9a, 0x98,
-                0xab, 0xe5, 0x87, 0x6f, 0x35, 0x6f, 0x72, 0x8a, 0xa0, 0x9e, 0x0c, 0xca, 0x9e, 0xfe,
-                0x05, 0x76, 0x1a, 0x33, 0x09, 0xaa, 0x88, 0xb2, 0xfa, 0x0e, 0xe2, 0xd0, 0x4c, 0x1c,
-                0x46, 0xe9, 0xf2, 0xa0, 0x48, 0xd5, 0x9d, 0x55, 0x65, 0xaf, 0xa6, 0xc3, 0xf1, 0x5b,
-                0xce, 0x70, 0x8d, 0xaa, 0xab, 0x7b, 0x34, 0x0e,
+                0x44, 0x6d, 0x67, 0x7c, 0x4c, 0xfe, 0xfd, 0x02, 0x4b, 0x0a, 0xeb, 0x37, 0xa5, 0x98,
+                0xcc, 0x2e, 0xb3, 0xd2, 0x9b, 0x02, 0x94, 0xfe, 0x5b, 0xb6, 0x97, 0x8e, 0x8b, 0x43,
+                0xd3, 0x2b, 0x2e, 0x4f, 0x09, 0x56, 0xac, 0xd1, 0x3e, 0x7e, 0x3a, 0x63, 0xa1, 0x8f,
+                0xca, 0x32, 0xd6, 0xab, 0x94, 0xb9, 0x4e, 0xd0, 0x33, 0xe9, 0xa1, 0x0f, 0xc5, 0x69,
+                0x28, 0xbc, 0x8a, 0x0f, 0x4f, 0x8e, 0x95, 0x00,
             ],
             rsig: [
-                0xc9, 0x66, 0x84, 0xec, 0x7e, 0xa6, 0x0b, 0xde, 0x87, 0x88, 0x22, 0xdd, 0xca, 0xf6,
-                0xb8, 0xb0, 0xbd, 0x31, 0x98, 0x51, 0x54, 0xdf, 0x9a, 0xd4, 0xf6, 0x90, 0x7d, 0xf8,
-                0xfe, 0xd9, 0x5c, 0x1d, 0x84, 0xfe, 0x67, 0xe6, 0x78, 0x75, 0xa5, 0x39, 0x55, 0x0e,
-                0xb2, 0x51, 0x4f, 0x19, 0x3b, 0x8e, 0xd4, 0x57, 0x25, 0x6c, 0x8d, 0x30, 0x28, 0x1d,
-                0x6f, 0x8b, 0xb9, 0x54, 0x49, 0x24, 0xca, 0x0c,
+                0x8d, 0xe0, 0x41, 0xe7, 0x09, 0xdb, 0x62, 0x4a, 0xe2, 0xbe, 0x16, 0x48, 0xb6, 0x62,
+                0x23, 0x9c, 0xde, 0xdf, 0x85, 0xec, 0xd3, 0x82, 0x26, 0x8b, 0x0e, 0x35, 0x54, 0xbf,
+                0xa0, 0xf2, 0x08, 0x1c, 0xd6, 0x41, 0xbc, 0xa0, 0x40, 0x78, 0xaa, 0x89, 0xf7, 0xdd,
+                0x25, 0x40, 0x58, 0x7c, 0xed, 0x6b, 0x45, 0x89, 0x16, 0xb1, 0x3e, 0x4b, 0x6a, 0x36,
+                0x30, 0xda, 0x69, 0x76, 0x46, 0xdb, 0xbf, 0x09,
             ],
         },
         TestVector {
@@ -418,18 +415,18 @@ fn redjubjub_signatures() {
                 0x08, 0x08, 0x08, 0x08,
             ],
             sig: [
-                0x24, 0x93, 0x2c, 0x1f, 0xaa, 0x01, 0x63, 0xca, 0x9a, 0x7f, 0xcd, 0xe4, 0x76, 0x11,
-                0x29, 0xd2, 0xe5, 0xe9, 0x9c, 0xf5, 0xef, 0xa2, 0x5d, 0x27, 0x04, 0x58, 0x8e, 0x1c,
-                0x75, 0x67, 0x7b, 0x5e, 0xeb, 0xe4, 0x55, 0x04, 0x8d, 0x7c, 0xe1, 0xb0, 0xd2, 0x01,
-                0x27, 0x53, 0xf7, 0x1b, 0x27, 0x25, 0x01, 0x2e, 0xe1, 0x85, 0x49, 0x28, 0x73, 0x18,
-                0xf9, 0xcd, 0x73, 0xf0, 0x7f, 0x0f, 0xb5, 0x02,
+                0x99, 0x35, 0x80, 0xef, 0x93, 0x34, 0x9a, 0x1c, 0x9e, 0xe9, 0x60, 0xca, 0x3e, 0x7c,
+                0xd0, 0x4c, 0x13, 0xb4, 0xa0, 0xec, 0x4f, 0xd1, 0x80, 0x53, 0xa1, 0x9c, 0xff, 0x77,
+                0x63, 0x62, 0x09, 0x65, 0xfb, 0xee, 0x96, 0xc1, 0x64, 0x72, 0x30, 0xe3, 0x73, 0xcb,
+                0x82, 0xb8, 0x1d, 0x00, 0x03, 0x92, 0x23, 0xd3, 0x0b, 0x39, 0x3e, 0xd1, 0x72, 0xc9,
+                0xb3, 0xc5, 0x63, 0xc6, 0x11, 0x79, 0x22, 0x05,
             ],
             rsig: [
-                0xf7, 0xfa, 0x26, 0xca, 0x22, 0xf3, 0x86, 0xc4, 0x3c, 0x19, 0x1a, 0x0b, 0x3e, 0xa6,
-                0x57, 0x7e, 0x8e, 0xea, 0xa3, 0xf3, 0x6b, 0x9b, 0xd1, 0xa3, 0xac, 0x3d, 0xf6, 0xf8,
-                0x83, 0xa3, 0xff, 0xdb, 0x31, 0x32, 0x0b, 0xde, 0x62, 0x7f, 0xf4, 0x6f, 0xc2, 0x26,
-                0x4a, 0x32, 0x63, 0xb9, 0xab, 0x67, 0x12, 0x3b, 0xa5, 0xe1, 0x08, 0x43, 0x20, 0xd9,
-                0x10, 0xb3, 0x94, 0xef, 0x8c, 0x65, 0xba, 0x09,
+                0xcc, 0x7a, 0xae, 0x1c, 0xed, 0xad, 0x2d, 0x7f, 0x6c, 0xe0, 0x4c, 0x19, 0xc5, 0xa5,
+                0xb6, 0xb7, 0xa6, 0xa0, 0x82, 0x78, 0x5c, 0x54, 0x0c, 0x14, 0xf6, 0x30, 0x9b, 0x06,
+                0x4d, 0x1f, 0xfa, 0x68, 0x17, 0x29, 0x53, 0xfb, 0xa0, 0xc2, 0xfc, 0xfb, 0x87, 0x5c,
+                0xa7, 0xf7, 0xea, 0x98, 0xef, 0x55, 0xa0, 0x40, 0x2f, 0xd5, 0x29, 0xcf, 0xcd, 0xdf,
+                0x99, 0x6c, 0xa2, 0xb8, 0xca, 0x89, 0x90, 0x0a,
             ],
         },
         TestVector {
@@ -464,47 +461,37 @@ fn redjubjub_signatures() {
                 0x09, 0x09, 0x09, 0x09,
             ],
             sig: [
-                0x64, 0xab, 0xd1, 0x25, 0xbf, 0xc4, 0xc6, 0x54, 0xfa, 0xf2, 0xb6, 0xdd, 0x75, 0x3e,
-                0xc6, 0x90, 0x22, 0x4d, 0xbc, 0xab, 0x8c, 0xd6, 0x32, 0xdd, 0x59, 0x3c, 0x91, 0xce,
-                0x3a, 0xb0, 0xbc, 0xad, 0xca, 0x92, 0x76, 0x34, 0x02, 0x1c, 0x31, 0x47, 0x6c, 0x78,
-                0xc5, 0xac, 0x7c, 0xcc, 0xab, 0xbd, 0x6f, 0x92, 0x7d, 0xf2, 0x05, 0xea, 0xa7, 0x07,
-                0xcc, 0x00, 0xd4, 0x7d, 0x39, 0xf3, 0xe4, 0x0c,
+                0xce, 0x90, 0xdd, 0xf4, 0xaf, 0x21, 0xaa, 0xc4, 0xd9, 0x41, 0x93, 0xea, 0x16, 0xff,
+                0x35, 0xcd, 0x93, 0x79, 0x20, 0x4e, 0x7d, 0x8f, 0xf4, 0xc0, 0xf5, 0x41, 0x17, 0xab,
+                0xb1, 0x6b, 0x7c, 0x85, 0xa0, 0xb1, 0x97, 0xcf, 0x13, 0xab, 0x14, 0xd7, 0xc3, 0xba,
+                0x68, 0x01, 0x0a, 0xb8, 0x05, 0x12, 0x25, 0x91, 0x3b, 0xdb, 0xc3, 0x9a, 0x51, 0xf6,
+                0x03, 0x7a, 0xfc, 0x6c, 0xee, 0xcb, 0x0b, 0x06,
             ],
             rsig: [
-                0xeb, 0x7a, 0x06, 0x5d, 0x75, 0xf8, 0x45, 0xdc, 0x09, 0x41, 0xb7, 0x09, 0xc0, 0xb1,
-                0x49, 0xea, 0xfd, 0x80, 0x5e, 0xa5, 0x8f, 0x38, 0x0b, 0x92, 0xb9, 0xd3, 0x10, 0x8a,
-                0x56, 0x1b, 0xda, 0x17, 0x85, 0xdf, 0x8f, 0x10, 0x1e, 0x0e, 0x14, 0x0f, 0xca, 0xee,
-                0x99, 0xb7, 0xdb, 0xb7, 0xdf, 0xbf, 0x7e, 0x61, 0xf3, 0xa1, 0x2f, 0x46, 0x09, 0x50,
-                0x69, 0xe0, 0x6e, 0x88, 0x96, 0xa9, 0xe4, 0x04,
+                0xa8, 0x47, 0x74, 0x2e, 0x94, 0x01, 0xcf, 0x22, 0x39, 0x21, 0x3d, 0xc8, 0x81, 0x3e,
+                0x97, 0x72, 0xe9, 0x7a, 0xf8, 0xd6, 0x7a, 0xdf, 0xfe, 0xab, 0xc8, 0xe6, 0x7f, 0x5d,
+                0x2d, 0x90, 0xd0, 0xb4, 0x1b, 0xc2, 0x5b, 0x05, 0xf9, 0x4a, 0xce, 0x16, 0x8a, 0xec,
+                0xc6, 0x58, 0x3e, 0x18, 0xf7, 0x63, 0x74, 0x92, 0xf3, 0x7a, 0x9c, 0xa3, 0x00, 0x20,
+                0x2b, 0xc0, 0x65, 0xab, 0xd3, 0x80, 0xec, 0x00,
             ],
         },
     ];
 
     for tv in test_vectors {
-        let sk = PrivateKey::read(&tv.sk[..]).unwrap();
-        let vk = PublicKey::read(&tv.vk[..]).unwrap();
-        let rvk = PublicKey::read(&tv.rvk[..]).unwrap();
-        let sig = Signature::read(&tv.sig[..]).unwrap();
-        let rsig = Signature::read(&tv.rsig[..]).unwrap();
+        let sk = redjubjub::SigningKey::try_from(tv.sk).unwrap();
+        let vk = redjubjub::VerificationKey::try_from(tv.vk).unwrap();
+        let rvk = redjubjub::VerificationKey::try_from(tv.rvk).unwrap();
+        let sig = redjubjub::Signature::from(tv.sig);
+        let rsig = redjubjub::Signature::from(tv.rsig);
 
         let alpha = jubjub::Scalar::from_bytes(&tv.alpha).unwrap();
 
-        {
-            let mut vec = Vec::new();
-            sk.randomize(alpha).write(&mut vec).unwrap();
-            assert_eq!(&vec, &tv.rsk);
-        }
-        {
-            let mut vec = Vec::new();
-            vk.randomize(alpha, SPENDING_KEY_GENERATOR)
-                .write(&mut vec)
-                .unwrap();
-            assert_eq!(&vec, &tv.rvk);
-        }
+        assert_eq!(<[u8; 32]>::from(sk.randomize(&alpha)), tv.rsk);
+        assert_eq!(<[u8; 32]>::from(vk.randomize(&alpha)), tv.rvk);
 
-        assert!(vk.verify(&tv.m, &sig, SPENDING_KEY_GENERATOR));
-        assert!(rvk.verify(&tv.m, &rsig, SPENDING_KEY_GENERATOR));
-        assert!(!vk.verify(&tv.m, &rsig, SPENDING_KEY_GENERATOR));
-        assert!(!rvk.verify(&tv.m, &sig, SPENDING_KEY_GENERATOR));
+        assert_eq!(vk.verify(&tv.m, &sig), Ok(()));
+        assert_eq!(rvk.verify(&tv.m, &rsig), Ok(()));
+        assert!(matches!(vk.verify(&tv.m, &rsig), Err(_)));
+        assert!(matches!(rvk.verify(&tv.m, &sig), Err(_)));
     }
 }
diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp
index 595ed58289f..ff19cfd059e 100644
--- a/src/test/coins_tests.cpp
+++ b/src/test/coins_tests.cpp
@@ -306,9 +306,10 @@ class TxWithNullifiers
 
         // The Orchard bundle builder always pads to two Actions, so we can just
         // use an empty builder to create a dummy Orchard bundle.
+        // TODO: With the new BundleType::DEFAULT this is no longer true. Fix this.
         uint256 orchardAnchor;
         uint256 dataToBeSigned;
-        auto builder = orchard::Builder(true, true, orchardAnchor);
+        auto builder = orchard::Builder(false, orchardAnchor);
         mutableTx.orchardBundle = builder.Build().value().ProveAndSign({}, dataToBeSigned).value();
         orchardNullifier = mutableTx.orchardBundle.GetNullifiers()[0];
 
@@ -340,7 +341,7 @@ template<> void AppendRandomLeaf(OrchardMerkleFrontier &tree) {
     // append a random leaf to OrchardMerkleFrontier.
     uint256 orchardAnchor;
     uint256 dataToBeSigned;
-    auto builder = orchard::Builder(true, true, orchardAnchor);
+    auto builder = orchard::Builder(false, orchardAnchor);
     auto bundle = builder.Build().value().ProveAndSign({}, dataToBeSigned).value();
     tree.AppendBundle(bundle);
 }
diff --git a/src/test/key_tests.cpp b/src/test/key_tests.cpp
index 4f414337162..eaad542d1c4 100644
--- a/src/test/key_tests.cpp
+++ b/src/test/key_tests.cpp
@@ -206,7 +206,7 @@ BOOST_AUTO_TEST_CASE(zs_address_test)
 
     KeyIO keyIO(Params());
     for (uint32_t i = 0; i < 1000; i++) {
-        auto sk = m.Derive(i);
+        auto sk = m.Derive(i | HARDENED_KEY_LIMIT);
         {
             std::string sk_string = keyIO.EncodeSpendingKey(sk);
             BOOST_CHECK(sk_string.compare(0, 27, Params().Bech32HRP(CChainParams::SAPLING_EXTENDED_SPEND_KEY)) == 0);
diff --git a/src/util/test.cpp b/src/util/test.cpp
index 9bb4ee387df..2f74ce8c8a8 100644
--- a/src/util/test.cpp
+++ b/src/util/test.cpp
@@ -352,8 +352,9 @@ CWalletTx GetValidSaplingReceive(const CChainParams& params,
     // To zaddr
     auto fvk = sk.expsk.full_viewing_key();
     auto pa = sk.ToXFVK().DefaultAddress();
+    auto saplingAnchor = SaplingMerkleTree::empty_root();
 
-    auto builder = TransactionBuilder(params, 1, std::nullopt, &keyStore);
+    auto builder = TransactionBuilder(params, 1, std::nullopt, saplingAnchor, &keyStore);
     builder.SetFee(0);
     builder.AddTransparentInput(COutPoint(), scriptPubKey, value);
     builder.AddSaplingOutput(fvk.ovk, pa, value, {});
diff --git a/src/wallet/gtest/test_orchard_wallet.cpp b/src/wallet/gtest/test_orchard_wallet.cpp
index a52d91f3fc1..25922427e0e 100644
--- a/src/wallet/gtest/test_orchard_wallet.cpp
+++ b/src/wallet/gtest/test_orchard_wallet.cpp
@@ -32,7 +32,7 @@ CTransaction FakeOrchardTx(const OrchardSpendingKey& sk, libzcash::diversifier_i
 
     // Create a shielding transaction from transparent to Orchard
     // 0.0005 t-ZEC in, 0.0004 z-ZEC out, 0.0001 fee
-    auto builder = TransactionBuilder(Params(), 1, orchardAnchor, &keystore);
+    auto builder = TransactionBuilder(Params(), 1, orchardAnchor, SaplingMerkleTree::empty_root(), &keystore);
     builder.SetFee(10000);
     builder.AddTransparentInput(COutPoint(uint256S("1234"), 0), scriptPubKey, 50000);
     builder.AddOrchardOutput(std::nullopt, recipient, 40000, std::nullopt);
@@ -129,7 +129,7 @@ TEST(TransactionBuilder, OrchardToOrchard) {
 
     // Create an Orchard-only transaction
     // 0.0004 z-ZEC in, 0.00025 z-ZEC out, default fee, 0.00014 z-ZEC change
-    auto builder = TransactionBuilder(Params(), 2, orchardAnchor);
+    auto builder = TransactionBuilder(Params(), 2, orchardAnchor, SaplingMerkleTree::empty_root());
     EXPECT_TRUE(builder.AddOrchardSpend(sk, std::move(spendInfo[0].second)));
     builder.AddOrchardOutput(std::nullopt, recipient, 25000, std::nullopt);
     auto maybeTx = builder.Build();
diff --git a/src/wallet/gtest/test_rpc_wallet.cpp b/src/wallet/gtest/test_rpc_wallet.cpp
index 749243c0e03..53737883369 100644
--- a/src/wallet/gtest/test_rpc_wallet.cpp
+++ b/src/wallet/gtest/test_rpc_wallet.cpp
@@ -330,65 +330,77 @@ TEST(WalletRPCTests, RPCZsendmanyTaddrToSapling)
     ss >> tx;
     ASSERT_NE(tx.GetSaplingOutputsCount(), 0);
 
-    auto outputs = tx.GetSaplingOutputs();
-    auto enc_ciphertext = outputs[0].enc_ciphertext();
-    auto out_ciphertext = outputs[0].out_ciphertext();
-    auto cv = outputs[0].cv();
-    auto cmu = outputs[0].cmu();
-    auto ephemeral_key = outputs[0].ephemeral_key();
-
-    // We shouldn't be able to decrypt with the empty ovk
-    EXPECT_THROW(wallet::try_sapling_output_recovery(
-        *rustNetwork,
-        nextBlockHeight,
-        uint256().GetRawBytes(),
-        {
-            cv,
-            cmu,
-            ephemeral_key,
-            enc_ciphertext,
-            out_ciphertext,
-        }), rust::Error);
-
-    // We shouldn't be able to decrypt with a random ovk
-    EXPECT_THROW(wallet::try_sapling_output_recovery(
-        *rustNetwork,
-        nextBlockHeight,
-        random_uint256().GetRawBytes(),
-        {
-            cv,
-            cmu,
-            ephemeral_key,
-            enc_ciphertext,
-            out_ciphertext,
-        }), rust::Error);
-
     auto accountKey = pwalletMain->GetLegacyAccountKey().ToAccountPubKey();
     auto ovks = accountKey.GetOVKsForShielding();
-    // We should not be able to decrypt with the internal change OVK for shielding
-    EXPECT_THROW(wallet::try_sapling_output_recovery(
-        *rustNetwork,
-        nextBlockHeight,
-        ovks.first.GetRawBytes(),
-        {
-            cv,
-            cmu,
-            ephemeral_key,
-            enc_ciphertext,
-            out_ciphertext,
-        }), rust::Error);
-    // We should be able to decrypt with the external OVK for shielding
-    EXPECT_NO_THROW(wallet::try_sapling_output_recovery(
-        *rustNetwork,
-        nextBlockHeight,
-        ovks.second.GetRawBytes(),
-        {
-            cv,
-            cmu,
-            ephemeral_key,
-            enc_ciphertext,
-            out_ciphertext,
-        }));
+
+    auto extDecryptSucceeded = 0;
+    auto extDecryptFailed = 0;
+    for (auto& output: tx.GetSaplingOutputs()) {
+        auto enc_ciphertext = output.enc_ciphertext();
+        auto out_ciphertext = output.out_ciphertext();
+        auto cv = output.cv();
+        auto cmu = output.cmu();
+        auto ephemeral_key = output.ephemeral_key();
+
+        // We shouldn't be able to decrypt with the empty ovk
+        EXPECT_THROW(wallet::try_sapling_output_recovery(
+            *rustNetwork,
+            nextBlockHeight,
+            uint256().GetRawBytes(),
+            {
+                cv,
+                cmu,
+                ephemeral_key,
+                enc_ciphertext,
+                out_ciphertext,
+            }), rust::Error);
+
+        // We shouldn't be able to decrypt with a random ovk
+        EXPECT_THROW(wallet::try_sapling_output_recovery(
+            *rustNetwork,
+            nextBlockHeight,
+            random_uint256().GetRawBytes(),
+            {
+                cv,
+                cmu,
+                ephemeral_key,
+                enc_ciphertext,
+                out_ciphertext,
+            }), rust::Error);
+
+        // We should not be able to decrypt with the internal change OVK for shielding
+        EXPECT_THROW(wallet::try_sapling_output_recovery(
+            *rustNetwork,
+            nextBlockHeight,
+            ovks.first.GetRawBytes(),
+            {
+                cv,
+                cmu,
+                ephemeral_key,
+                enc_ciphertext,
+                out_ciphertext,
+            }), rust::Error);
+
+        // We should be able to decrypt one of the outputs with the external OVK for shielding.
+        try {
+            wallet::try_sapling_output_recovery(
+                *rustNetwork,
+                nextBlockHeight,
+                ovks.second.GetRawBytes(),
+                {
+                    cv,
+                    cmu,
+                    ephemeral_key,
+                    enc_ciphertext,
+                    out_ciphertext,
+                });
+            extDecryptSucceeded += 1;
+        } catch (...) {
+            extDecryptFailed += 1;
+        }
+    }
+    EXPECT_EQ(extDecryptSucceeded, 1);
+    EXPECT_EQ(extDecryptFailed, 1);
 
     // Tear down
     chainActive.SetTip(NULL);
diff --git a/src/wallet/gtest/test_wallet.cpp b/src/wallet/gtest/test_wallet.cpp
index 0b4f080d03c..9fc8b420e26 100644
--- a/src/wallet/gtest/test_wallet.cpp
+++ b/src/wallet/gtest/test_wallet.cpp
@@ -421,13 +421,14 @@ TEST(WalletTests, SetSaplingNoteAddrsInCWalletTx) {
         auto cm = note.cmu().value();
         SaplingMerkleTree tree;
         tree.append(cm);
+        auto anchor = tree.root();
         auto witness = tree.witness();
 
         auto nf = note.nullifier(fvk, witness.position());
         ASSERT_TRUE(nf);
         uint256 nullifier = nf.value();
 
-        auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+        auto builder = TransactionBuilder(Params(), 1, std::nullopt, anchor);
         builder.AddSaplingSpend(sk, note, witness);
         builder.AddSaplingOutput(fvk.ovk, pk, 50000, {});
         builder.SetFee(0);
@@ -564,7 +565,7 @@ TEST(WalletTests, FindMySaplingNotes) {
     auto testNote = GetTestSaplingNote(pa, 50000);
 
     // Generate transaction
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root());
     builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
     builder.AddSaplingOutput(extfvk.fvk.ovk, pa, 25000, {});
     auto tx = builder.Build().GetTxOrThrow();
@@ -708,10 +709,11 @@ TEST(WalletTests, GetConflictedSaplingNotes) {
 
         MerkleFrontiers frontiers;
         frontiers.sapling.append(cm);
+        auto anchor = frontiers.sapling.root();
         auto witness = frontiers.sapling.witness();
 
         // Generate tx to create output note B
-        auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+        auto builder = TransactionBuilder(Params(), 1, std::nullopt, anchor);
         builder.AddSaplingSpend(sk, note, witness);
         builder.AddSaplingOutput(extfvk.fvk.ovk, pk, 35000, {});
         auto tx = builder.Build().GetTxOrThrow();
@@ -758,13 +760,13 @@ TEST(WalletTests, GetConflictedSaplingNotes) {
         auto nullifier2 = maybe_nf.value();
 
         // Create transaction to spend note B
-        auto builder2 = TransactionBuilder(Params(), 2, std::nullopt);
+        auto builder2 = TransactionBuilder(Params(), 2, std::nullopt, spend_note_witness.root());
         builder2.AddSaplingSpend(sk, note2, spend_note_witness);
         builder2.AddSaplingOutput(extfvk.fvk.ovk, pk, 2000, {});
         auto tx2 = builder2.Build().GetTxOrThrow();
 
         // Create conflicting transaction which also spends note B
-        auto builder3 = TransactionBuilder(Params(), 2, std::nullopt);
+        auto builder3 = TransactionBuilder(Params(), 2, std::nullopt, spend_note_witness.root());
         builder3.AddSaplingSpend(sk, note2, spend_note_witness);
         builder3.AddSaplingOutput(extfvk.fvk.ovk, pk, 1999, {});
         auto tx3 = builder3.Build().GetTxOrThrow();
@@ -822,7 +824,7 @@ TEST(WalletTests, GetConflictedOrchardNotes) {
     auto scriptPubKey = GetScriptForDestination(tkeyid);
 
     // Generate a bundle containing output note A.
-    auto builder = TransactionBuilder(Params(), 1, orchardAnchor, &keystore);
+    auto builder = TransactionBuilder(Params(), 1, orchardAnchor, SaplingMerkleTree::empty_root(), &keystore);
     builder.AddTransparentInput(COutPoint(uint256(), 0), scriptPubKey, 5000);
     builder.AddOrchardOutput(std::nullopt, recipient, 4000, {});
     auto maybeTx = builder.Build();
@@ -876,7 +878,7 @@ TEST(WalletTests, GetConflictedOrchardNotes) {
     auto recipient2 = ivk.Address(j2);
 
     // Generate tx to spend note A
-    auto builder2 = TransactionBuilder(Params(), 2, orchardTree.root());
+    auto builder2 = TransactionBuilder(Params(), 2, orchardTree.root(), SaplingMerkleTree::empty_root());
     auto noteToSpend = std::move(wallet.GetOrchardSpendInfo(orchardEntries, 1, orchardTree.root())[0]);
     builder2.AddOrchardSpend(std::move(noteToSpend.first), std::move(noteToSpend.second));
     auto maybeTx2 = builder2.Build();
@@ -890,7 +892,7 @@ TEST(WalletTests, GetConflictedOrchardNotes) {
 
     // Generate conflicting tx to spend note A
     auto noteToSpend2 = std::move(wallet.GetOrchardSpendInfo(orchardEntries, 1, orchardTree.root())[0]);
-    auto builder3 = TransactionBuilder(Params(), 2, orchardTree.root());
+    auto builder3 = TransactionBuilder(Params(), 2, orchardTree.root(), SaplingMerkleTree::empty_root());
     builder3.AddOrchardSpend(std::move(noteToSpend2.first), std::move(noteToSpend2.second));
     auto maybeTx3 = builder3.Build();
     EXPECT_TRUE(maybeTx3.IsTx());
@@ -992,7 +994,7 @@ TEST(WalletTests, SaplingNullifierIsSpent) {
     auto testNote = GetTestSaplingNote(pa, 50000);
 
     // Generate transaction
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root());
     builder.AddSaplingSpend(sk,  testNote.note, testNote.tree.witness());
     builder.AddSaplingOutput(extfvk.fvk.ovk, pa, 25000, {});
     auto tx = builder.Build().GetTxOrThrow();
@@ -1079,7 +1081,7 @@ TEST(WalletTests, NavigateFromSaplingNullifierToNote) {
     auto testNote = GetTestSaplingNote(pa, 50000);
 
     // Generate transaction
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root());
     builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
     builder.AddSaplingOutput(extfvk.fvk.ovk, pa, 25000, {});
     auto tx = builder.Build().GetTxOrThrow();
@@ -1214,10 +1216,11 @@ TEST(WalletTests, SpentSaplingNoteIsFromMe) {
         auto cm = note.cmu().value();
         MerkleFrontiers frontiers;
         frontiers.sapling.append(cm);
+        auto anchor = frontiers.sapling.root();
         auto witness = frontiers.sapling.witness();
 
         // Generate transaction, which sends funds to note B
-        auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+        auto builder = TransactionBuilder(Params(), 1, std::nullopt, anchor);
         builder.AddSaplingSpend(sk, note, witness);
         builder.AddSaplingOutput(extfvk.fvk.ovk, pk, 25000, {});
         auto tx = builder.Build().GetTxOrThrow();
@@ -1278,7 +1281,7 @@ TEST(WalletTests, SpentSaplingNoteIsFromMe) {
         auto nullifier2 = maybe_nf.value();
 
         // Create transaction to spend note B
-        auto builder2 = TransactionBuilder(Params(), 2, std::nullopt);
+        auto builder2 = TransactionBuilder(Params(), 2, std::nullopt, spend_note_witness.root());
         builder2.AddSaplingSpend(sk, note2, spend_note_witness);
         builder2.AddSaplingOutput(extfvk.fvk.ovk, pk, 12500, {});
         auto tx2 = builder2.Build().GetTxOrThrow();
@@ -2046,13 +2049,13 @@ TEST(WalletTests, UpdatedSaplingNoteData) {
     auto m = GetTestMasterSaplingSpendingKey();
 
     // Generate dummy Sapling address
-    auto sk = m.Derive(0);
+    auto sk = m.Derive(0 | HARDENED_KEY_LIMIT);
     auto expsk = sk.expsk;
     auto extfvk = sk.ToXFVK();
     auto pa = extfvk.DefaultAddress();
 
     // Generate dummy recipient Sapling address
-    auto sk2 = m.Derive(1);
+    auto sk2 = m.Derive(1 | HARDENED_KEY_LIMIT);
     auto expsk2 = sk2.expsk;
     auto extfvk2 = sk2.ToXFVK();
     auto pa2 = extfvk2.DefaultAddress();
@@ -2060,7 +2063,7 @@ TEST(WalletTests, UpdatedSaplingNoteData) {
     auto testNote = GetTestSaplingNote(pa, 50000);
 
     // Generate transaction
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, testNote.tree.root());
     builder.AddSaplingSpend(sk, testNote.note, testNote.tree.witness());
     builder.AddSaplingOutput(extfvk.fvk.ovk, pa2, 25000, {});
     auto tx = builder.Build().GetTxOrThrow();
@@ -2223,7 +2226,7 @@ TEST(WalletTests, MarkAffectedSaplingTransactionsDirty) {
 
     // Generate shielding tx from transparent to Sapling
     // 0.0005 t-ZEC in, 0.0004 z-ZEC out, default fee
-    auto builder = TransactionBuilder(Params(), 1, std::nullopt, &keystore);
+    auto builder = TransactionBuilder(Params(), 1, std::nullopt, SaplingMerkleTree::empty_root(), &keystore);
     builder.AddTransparentInput(
         COutPoint(uint256S("7777777777777777777777777777777777777777777777777777777777777777"), 0),
         scriptPubKey, 5000);
@@ -2234,7 +2237,7 @@ TEST(WalletTests, MarkAffectedSaplingTransactionsDirty) {
     EXPECT_EQ(tx1.vout.size(), 0);
     EXPECT_EQ(tx1.vJoinSplit.size(), 0);
     EXPECT_EQ(tx1.GetSaplingSpendsCount(), 0);
-    EXPECT_EQ(tx1.GetSaplingOutputsCount(), 1);
+    EXPECT_EQ(tx1.GetSaplingOutputsCount(), 2);
     EXPECT_EQ(tx1.GetValueBalanceSapling(), -4000);
 
     CWalletTx wtx {&wallet, tx1};
@@ -2269,17 +2272,23 @@ TEST(WalletTests, MarkAffectedSaplingTransactionsDirty) {
     wtx = wallet.mapWallet[hash];
 
     // Prepare to spend the note that was just created
-    auto maybe_pt = wtx.DecryptSaplingNote(Params(), SaplingOutPoint(hash, 0));
+    auto outpt = SaplingOutPoint(hash, 0);
+    auto maybe_pt = wtx.DecryptSaplingNote(Params(), outpt);
+    if (!static_cast<bool>(maybe_pt)) {
+        outpt = SaplingOutPoint(hash, 1);
+        maybe_pt = wtx.DecryptSaplingNote(Params(), outpt);
+    }
     ASSERT_EQ(static_cast<bool>(maybe_pt), true);
     auto maybe_note = maybe_pt.value().first.note(ivk);
     ASSERT_EQ(static_cast<bool>(maybe_note), true);
     auto note = maybe_note.value();
     auto anchor = frontiers.sapling.root();
-    auto witness = frontiers.sapling.witness();
+    auto witness = wtx.mapSaplingNoteData[outpt].witnesses.back();
+    ASSERT_EQ(anchor, witness.root());
 
     // Create a Sapling-only transaction
     // 0.0004 z-ZEC in, 0.00025 z-ZEC out, default fee, 0.00005 z-ZEC change
-    auto builder2 = TransactionBuilder(Params(), 2, std::nullopt);
+    auto builder2 = TransactionBuilder(Params(), 2, std::nullopt, anchor);
     builder2.AddSaplingSpend(sk, note, witness);
     builder2.AddSaplingOutput(extfvk.fvk.ovk, pk, 2500, {});
     auto tx2 = builder2.Build().GetTxOrThrow();
diff --git a/src/wallet/gtest/test_wallet_zkeys.cpp b/src/wallet/gtest/test_wallet_zkeys.cpp
index 34ee9b15c88..15e121cbbd0 100644
--- a/src/wallet/gtest/test_wallet_zkeys.cpp
+++ b/src/wallet/gtest/test_wallet_zkeys.cpp
@@ -48,7 +48,7 @@ TEST(WalletZkeysTest, StoreAndLoadSaplingZkeys) {
 
     // manually add new spending key to wallet
     auto m = libzcash::SaplingExtendedSpendingKey::Master(seed);
-    auto sk = m.Derive(0);
+    auto sk = m.Derive(0 | HARDENED_KEY_LIMIT);
     ASSERT_TRUE(wallet.AddSaplingZKey(sk));
 
     // verify wallet did add it
@@ -88,7 +88,7 @@ TEST(WalletZkeysTest, StoreAndLoadSaplingZkeys) {
     EXPECT_TRUE(wallet.HaveSaplingIncomingViewingKey(dpa));
 
     // Load a third key into the wallet
-    auto sk2 = m.Derive(1);
+    auto sk2 = m.Derive(1 | HARDENED_KEY_LIMIT);
     ASSERT_TRUE(wallet.LoadSaplingZKey(sk2));
 
     // attach metadata to this third key
diff --git a/src/wallet/test/rpc_wallet_tests.cpp b/src/wallet/test/rpc_wallet_tests.cpp
index 0ed6cd9038e..b64f8db12d3 100644
--- a/src/wallet/test/rpc_wallet_tests.cpp
+++ b/src/wallet/test/rpc_wallet_tests.cpp
@@ -691,7 +691,7 @@ BOOST_AUTO_TEST_CASE(rpc_wallet_z_importexport)
         BOOST_CHECK_EQUAL(retValue.get_str(), testKey);
 
         // create a random Sapling key locally; split between IVKs and spending keys.
-        auto testSaplingSpendingKey = m.Derive(i);
+        auto testSaplingSpendingKey = m.Derive(i | HARDENED_KEY_LIMIT);
         auto testSaplingPaymentAddress = testSaplingSpendingKey.ToXFVK().DefaultAddress();
         if (i % 2 == 0) {
             std::string testSaplingAddr = keyIO.EncodePaymentAddress(testSaplingPaymentAddress);
@@ -1204,7 +1204,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_parameters)
     // Mutable tx containing contextual information we need to build tx
     UniValue retValue = CallRPC("getblockcount");
     int nHeight = retValue.get_int();
-    TransactionBuilder builder(Params(), nHeight + 1, std::nullopt, pwalletMain);
+    TransactionBuilder builder(Params(), nHeight + 1, std::nullopt, SaplingMerkleTree::empty_root(), pwalletMain);
 }
 
 BOOST_AUTO_TEST_CASE(asyncrpcoperation_sign_send_raw_transaction) {