diff --git a/.github/workflows/ci-basic.yml b/.github/workflows/ci-basic.yml index 1d273173929..31000553d65 100644 --- a/.github/workflows/ci-basic.yml +++ b/.github/workflows/ci-basic.yml @@ -42,7 +42,23 @@ jobs: - name: Run format check run: cargo fmt -- --check - name: Run clippy - run: cargo clippy --workspace --no-deps --all-features --all-targets --locked -- -D warnings + run: | + # After upgrading from Rust 1.82 to 1.85.1, Clippy started failing on new lints in upstream + # code we don’t want to patch here (and may already be fixed upstream). To avoid carrying + # fixes and merge conflicts, we allow these lints only for the affected crates for now. + # TODO: remove this comment and these Clippy exceptions after syncing with updated upstream. + cargo clippy --workspace --no-deps --all-features --all-targets --locked -- \ + -D warnings \ + -A elided_named_lifetimes \ + -A missing_docs \ + -A non_local_definitions \ + -A clippy::needless_lifetimes \ + -A clippy::needless_return \ + -A clippy::unnecessary_lazy_evaluations \ + -A clippy::unnecessary_map_or \ + -A clippy::needless_as_bytes \ + -A clippy::useless_conversion + - name: Show system resource summary run: | df -h diff --git a/Cargo.lock b/Cargo.lock index 29530543715..f71b844e33e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,12 +339,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - [[package]] name = "base64" version = "0.11.0" @@ -501,13 +495,13 @@ dependencies = [ [[package]] name = "blake2b_simd" -version = "1.0.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +checksum = "3c2f0dc9a68c6317d884f97cc36cf5a3d20ba14ce404227df55e1af708ab04bc" dependencies = [ "arrayref", "arrayvec", - "constant_time_eq", + "constant_time_eq 0.2.6", ] [[package]] @@ -518,7 +512,7 @@ checksum = "94230421e395b9920d23df13ea5d77a20e1725331f90fbbf6df6040b33f756ae" dependencies = [ "arrayref", "arrayvec", - "constant_time_eq", + "constant_time_eq 0.3.0", ] [[package]] @@ -954,6 +948,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" + [[package]] name = "constant_time_eq" version = "0.3.0" @@ -1079,18 +1079,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - [[package]] name = "crypto-common" version = "0.1.6" @@ -1245,7 +1233,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid", "crypto-common 0.1.6", ] @@ -1296,18 +1283,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "signature", -] - [[package]] name = "ed25519" version = "2.2.3" @@ -1361,24 +1336,6 @@ dependencies = [ "void", ] -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - [[package]] name = "encode_unicode" version = "0.3.6" @@ -1413,7 +1370,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.2.2" -source = "git+https://github.com/QED-it/librustzcash?rev=e27124a49a0e549227e12adf13e42f88593f1dee#e27124a49a0e549227e12adf13e42f88593f1dee" +source = "git+https://github.com/QED-it/librustzcash?rev=acd68d123997ea9788c14244bc3805c6d2e8d68c#acd68d123997ea9788c14244bc3805c6d2e8d68c" dependencies = [ "blake2b_simd", "core2", @@ -1449,7 +1406,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.1" -source = "git+https://github.com/QED-it/librustzcash?rev=e27124a49a0e549227e12adf13e42f88593f1dee#e27124a49a0e549227e12adf13e42f88593f1dee" +source = "git+https://github.com/QED-it/librustzcash?rev=acd68d123997ea9788c14244bc3805c6d2e8d68c#acd68d123997ea9788c14244bc3805c6d2e8d68c" dependencies = [ "blake2b_simd", ] @@ -1665,7 +1622,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -1803,8 +1759,9 @@ dependencies = [ [[package]] name = "halo2_gadgets" -version = "0.3.1" -source = "git+https://github.com/zcash/halo2?rev=2308caf68c48c02468b66cfc452dad54e355e32f#2308caf68c48c02468b66cfc452dad54e355e32f" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45824ce0dd12e91ec0c68ebae2a7ed8ae19b70946624c849add59f1d1a62a143" dependencies = [ "arrayvec", "bitvec", @@ -2486,19 +2443,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if 1.0.0", - "ecdsa", - "elliptic-curve", - "sha2 0.10.8", - "signature", -] - [[package]] name = "known-folders" version = "1.1.0" @@ -2822,12 +2766,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nonempty" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" - [[package]] name = "nonempty" version = "0.11.0" @@ -2950,7 +2888,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orchard" version = "0.11.0" -source = "git+https://github.com/QED-it/orchard?rev=2083efe8d57e6073914ae296db2d41f8bfe1de50#2083efe8d57e6073914ae296db2d41f8bfe1de50" +source = "git+https://github.com/QED-it/orchard?rev=7ec34c9be7d36ecdce7c3658efe9a78c2863ed97#7ec34c9be7d36ecdce7c3658efe9a78c2863ed97" dependencies = [ "aes", "bitvec", @@ -2965,15 +2903,15 @@ dependencies = [ "halo2_proofs", "hex", "incrementalmerkletree", - "k256", "lazy_static", "memuse", - "nonempty 0.11.0", + "nonempty", "pasta_curves", "proptest", "rand 0.8.5", "rand_core 0.6.4", "reddsa", + "secp256k1 0.29.1", "serde", "sinsemilla", "subtle", @@ -4050,19 +3988,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "subtle", - "zeroize", -] - [[package]] name = "secp256k1" version = "0.27.0" @@ -4079,6 +4004,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ + "rand 0.8.5", "secp256k1-sys 0.10.1", ] @@ -4425,7 +4351,6 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", "rand_core 0.6.4", ] @@ -5928,7 +5853,7 @@ checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" [[package]] name = "zcash_address" version = "0.9.0" -source = "git+https://github.com/QED-it/librustzcash?rev=e27124a49a0e549227e12adf13e42f88593f1dee#e27124a49a0e549227e12adf13e42f88593f1dee" +source = "git+https://github.com/QED-it/librustzcash?rev=acd68d123997ea9788c14244bc3805c6d2e8d68c#acd68d123997ea9788c14244bc3805c6d2e8d68c" dependencies = [ "bech32", "bs58", @@ -5941,7 +5866,7 @@ dependencies = [ [[package]] name = "zcash_client_backend" version = "0.20.0" -source = "git+https://github.com/QED-it/librustzcash?rev=e27124a49a0e549227e12adf13e42f88593f1dee#e27124a49a0e549227e12adf13e42f88593f1dee" +source = "git+https://github.com/QED-it/librustzcash?rev=acd68d123997ea9788c14244bc3805c6d2e8d68c#acd68d123997ea9788c14244bc3805c6d2e8d68c" dependencies = [ "base64 0.22.1", "bech32", @@ -5954,7 +5879,7 @@ dependencies = [ "hex", "incrementalmerkletree", "memuse", - "nonempty 0.11.0", + "nonempty", "percent-encoding", "prost", "rand_core 0.6.4", @@ -5983,16 +5908,16 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.3.0" -source = "git+https://github.com/QED-it/librustzcash?rev=e27124a49a0e549227e12adf13e42f88593f1dee#e27124a49a0e549227e12adf13e42f88593f1dee" +source = "git+https://github.com/QED-it/librustzcash?rev=acd68d123997ea9788c14244bc3805c6d2e8d68c#acd68d123997ea9788c14244bc3805c6d2e8d68c" dependencies = [ "core2", - "nonempty 0.11.0", + "nonempty", ] [[package]] name = "zcash_history" version = "0.4.0" -source = "git+https://github.com/QED-it/librustzcash?rev=e27124a49a0e549227e12adf13e42f88593f1dee#e27124a49a0e549227e12adf13e42f88593f1dee" +source = "git+https://github.com/QED-it/librustzcash?rev=acd68d123997ea9788c14244bc3805c6d2e8d68c#acd68d123997ea9788c14244bc3805c6d2e8d68c" dependencies = [ "blake2b_simd", "byteorder", @@ -6002,7 +5927,7 @@ dependencies = [ [[package]] name = "zcash_keys" version = "0.11.0" -source = "git+https://github.com/QED-it/librustzcash?rev=e27124a49a0e549227e12adf13e42f88593f1dee#e27124a49a0e549227e12adf13e42f88593f1dee" +source = "git+https://github.com/QED-it/librustzcash?rev=acd68d123997ea9788c14244bc3805c6d2e8d68c#acd68d123997ea9788c14244bc3805c6d2e8d68c" dependencies = [ "bech32", "blake2b_simd", @@ -6012,7 +5937,7 @@ dependencies = [ "document-features", "group", "memuse", - "nonempty 0.11.0", + "nonempty", "rand_core 0.6.4", "sapling-crypto", "secrecy", @@ -6040,7 +5965,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.25.0" -source = "git+https://github.com/QED-it/librustzcash?rev=e27124a49a0e549227e12adf13e42f88593f1dee#e27124a49a0e549227e12adf13e42f88593f1dee" +source = "git+https://github.com/QED-it/librustzcash?rev=acd68d123997ea9788c14244bc3805c6d2e8d68c#acd68d123997ea9788c14244bc3805c6d2e8d68c" dependencies = [ "bip32", "blake2b_simd", @@ -6059,7 +5984,7 @@ dependencies = [ "jubjub", "lazy_static", "memuse", - "nonempty 0.11.0", + "nonempty", "orchard", "rand 0.8.5", "rand_core 0.6.4", @@ -6082,7 +6007,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.25.0" -source = "git+https://github.com/QED-it/librustzcash?rev=e27124a49a0e549227e12adf13e42f88593f1dee#e27124a49a0e549227e12adf13e42f88593f1dee" +source = "git+https://github.com/QED-it/librustzcash?rev=acd68d123997ea9788c14244bc3805c6d2e8d68c#acd68d123997ea9788c14244bc3805c6d2e8d68c" dependencies = [ "bellman", "blake2b_simd", @@ -6104,7 +6029,7 @@ dependencies = [ [[package]] name = "zcash_protocol" version = "0.6.2" -source = "git+https://github.com/QED-it/librustzcash?rev=e27124a49a0e549227e12adf13e42f88593f1dee#e27124a49a0e549227e12adf13e42f88593f1dee" +source = "git+https://github.com/QED-it/librustzcash?rev=acd68d123997ea9788c14244bc3805c6d2e8d68c#acd68d123997ea9788c14244bc3805c6d2e8d68c" dependencies = [ "core2", "document-features", @@ -6139,8 +6064,7 @@ dependencies = [ [[package]] name = "zcash_spec" version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded3f58b93486aa79b85acba1001f5298f27a46489859934954d262533ee2915" +source = "git+https://github.com/QED-it/zcash_spec?rev=d5e84264d2ad0646b587a837f4e2424ca64d3a05#d5e84264d2ad0646b587a837f4e2424ca64d3a05" dependencies = [ "blake2b_simd", ] @@ -6148,7 +6072,7 @@ dependencies = [ [[package]] name = "zcash_transparent" version = "0.5.0" -source = "git+https://github.com/QED-it/librustzcash?rev=e27124a49a0e549227e12adf13e42f88593f1dee#e27124a49a0e549227e12adf13e42f88593f1dee" +source = "git+https://github.com/QED-it/librustzcash?rev=acd68d123997ea9788c14244bc3805c6d2e8d68c#acd68d123997ea9788c14244bc3805c6d2e8d68c" dependencies = [ "bip32", "blake2b_simd", @@ -6196,7 +6120,6 @@ dependencies = [ "itertools 0.13.0", "jubjub", "lazy_static", - "nonempty 0.7.0", "num-integer", "orchard", "primitive-types", @@ -6690,7 +6613,7 @@ dependencies = [ [[package]] name = "zip321" version = "0.5.0" -source = "git+https://github.com/QED-it/librustzcash?rev=e27124a49a0e549227e12adf13e42f88593f1dee#e27124a49a0e549227e12adf13e42f88593f1dee" +source = "git+https://github.com/QED-it/librustzcash?rev=acd68d123997ea9788c14244bc3805c6d2e8d68c#acd68d123997ea9788c14244bc3805c6d2e8d68c" dependencies = [ "base64 0.22.1", "nom", diff --git a/Cargo.toml b/Cargo.toml index c6d6f3e6d6a..fd87f13904f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ resolver = "2" [workspace.dependencies] incrementalmerkletree = "0.8.2" -orchard = "0.11.0" +orchard = { version = "0.11.0", features = ["zsa-issuance", "temporary-zebra"] } sapling-crypto = "0.5" # The dependency versions below are in accordance with the currently used orchard version. zcash_history = "0.4.0" @@ -31,7 +31,7 @@ zcash_address = "0.9.0" zcash_client_backend = "0.20.0" zcash_encoding = "0.3.0" zcash_keys = "0.11.0" -zcash_primitives = "0.25.0" +zcash_primitives = { version = "0.25.0", features = ["zsa-issuance"] } zcash_proofs = "0.25.0" zcash_protocol = "0.6.2" zcash_transparent = "0.5.0" @@ -108,19 +108,19 @@ lto = "thin" [patch.crates-io] halo2_proofs = { version = "0.3.0", git = "https://github.com/zcash/halo2", rev = "2308caf68c48c02468b66cfc452dad54e355e32f" } -halo2_gadgets = { version = "0.3.0", git = "https://github.com/zcash/halo2", rev = "2308caf68c48c02468b66cfc452dad54e355e32f" } halo2_poseidon = { version = "0.1.0", git = "https://github.com/zcash/halo2", rev = "2308caf68c48c02468b66cfc452dad54e355e32f" } sinsemilla = { git = "https://github.com/zcash/sinsemilla", rev = "aabb707e862bc3d7b803c77d14e5a771bcee3e8c" } zcash_note_encryption = { version = "0.4.1", git = "https://github.com/zcash/zcash_note_encryption", rev = "9f7e93d42cef839d02b9d75918117941d453f8cb" } sapling-crypto = { package = "sapling-crypto", version = "0.5", git = "https://github.com/QED-it/sapling-crypto", rev = "9393f93fe547d1b3738c9f4618c0f8a2fffed29f" } -orchard = { version = "0.11.0", git = "https://github.com/QED-it/orchard", rev = "2083efe8d57e6073914ae296db2d41f8bfe1de50" } -zcash_primitives = { version = "0.25.0", git = "https://github.com/QED-it/librustzcash", rev = "e27124a49a0e549227e12adf13e42f88593f1dee" } -zcash_protocol = { version = "0.6.2", git = "https://github.com/QED-it/librustzcash", rev = "e27124a49a0e549227e12adf13e42f88593f1dee" } -zcash_address = { version = "0.9.0", git = "https://github.com/QED-it/librustzcash", rev = "e27124a49a0e549227e12adf13e42f88593f1dee" } -zcash_encoding = { version = "0.3.0", git = "https://github.com/QED-it/librustzcash", rev = "e27124a49a0e549227e12adf13e42f88593f1dee" } -zcash_history = { version = "0.4.0", git = "https://github.com/QED-it/librustzcash", rev = "e27124a49a0e549227e12adf13e42f88593f1dee" } -zcash_client_backend = { version = "0.20.0", git = "https://github.com/QED-it/librustzcash", rev = "e27124a49a0e549227e12adf13e42f88593f1dee" } -zcash_keys = { version = "0.11.0", git = "https://github.com/QED-it/librustzcash", rev = "e27124a49a0e549227e12adf13e42f88593f1dee" } -zcash_transparent = { version = "0.5.0", git = "https://github.com/QED-it/librustzcash", rev = "e27124a49a0e549227e12adf13e42f88593f1dee" } -zcash_proofs = { version = "0.25.0", git = "https://github.com/QED-it/librustzcash", rev = "e27124a49a0e549227e12adf13e42f88593f1dee" } -equihash = { version = "0.2.2", git = "https://github.com/QED-it/librustzcash", rev = "e27124a49a0e549227e12adf13e42f88593f1dee" } +orchard = { version = "0.11.0", git = "https://github.com/QED-it/orchard", rev = "7ec34c9be7d36ecdce7c3658efe9a78c2863ed97" } +zcash_primitives = { version = "0.25.0", git = "https://github.com/QED-it/librustzcash", rev = "acd68d123997ea9788c14244bc3805c6d2e8d68c" } +zcash_protocol = { version = "0.6.2", git = "https://github.com/QED-it/librustzcash", rev = "acd68d123997ea9788c14244bc3805c6d2e8d68c" } +zcash_address = { version = "0.9.0", git = "https://github.com/QED-it/librustzcash", rev = "acd68d123997ea9788c14244bc3805c6d2e8d68c" } +zcash_encoding = { version = "0.3.0", git = "https://github.com/QED-it/librustzcash", rev = "acd68d123997ea9788c14244bc3805c6d2e8d68c" } +zcash_history = { version = "0.4.0", git = "https://github.com/QED-it/librustzcash", rev = "acd68d123997ea9788c14244bc3805c6d2e8d68c" } +zcash_client_backend = { version = "0.20.0", git = "https://github.com/QED-it/librustzcash", rev = "acd68d123997ea9788c14244bc3805c6d2e8d68c" } +zcash_keys = { version = "0.11.0", git = "https://github.com/QED-it/librustzcash", rev = "acd68d123997ea9788c14244bc3805c6d2e8d68c" } +zcash_transparent = { version = "0.5.0", git = "https://github.com/QED-it/librustzcash", rev = "acd68d123997ea9788c14244bc3805c6d2e8d68c" } +zcash_proofs = { version = "0.25.0", git = "https://github.com/QED-it/librustzcash", rev = "acd68d123997ea9788c14244bc3805c6d2e8d68c" } +equihash = { version = "0.2.2", git = "https://github.com/QED-it/librustzcash", rev = "acd68d123997ea9788c14244bc3805c6d2e8d68c" } +zcash_spec = { git = "https://github.com/QED-it/zcash_spec", rev = "d5e84264d2ad0646b587a837f4e2424ca64d3a05" } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index dd1c8aa4359..8a487d845cb 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ # TODO: Upstream specifies `channel = "stable"` — consider restoring it before final merge. [toolchain] -channel = "1.82.0" +channel = "1.85.1" diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index f202955e0b4..c5b9ceeaa16 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -62,9 +62,7 @@ proptest-impl = [ bench = ["zebra-test"] # Support for transaction version 6 -tx_v6 = [ - "nonempty" -] +tx_v6 = [] [dependencies] @@ -72,7 +70,7 @@ tx_v6 = [ bitvec = "1.0.1" bitflags = "2.5.0" bitflags-serde-legacy = "0.1.1" -blake2b_simd = "1.0.2" +blake2b_simd = "=1.0.1" blake2s_simd = "1.0.2" bridgetree = "0.7.0" bs58 = { version = "0.5.1", features = ["check"] } @@ -112,9 +110,6 @@ zcash_address.workspace = true zcash_transparent.workspace = true zcash_script.workspace = true -# Used for orchard serialization -nonempty = { version = "0.7", optional = true } - # Time chrono = { version = "0.4.38", default-features = false, features = ["clock", "std", "serde"] } humantime = "2.1.0" diff --git a/zebra-chain/src/orchard/commitment.rs b/zebra-chain/src/orchard/commitment.rs index c97e5053f20..d483f7a769d 100644 --- a/zebra-chain/src/orchard/commitment.rs +++ b/zebra-chain/src/orchard/commitment.rs @@ -236,7 +236,6 @@ impl ValueCommitment { /// Generate a new _ValueCommitment_. /// /// - #[allow(clippy::unwrap_in_result)] pub fn randomized(csprng: &mut T, value: Amount) -> Result where T: RngCore + CryptoRng, @@ -244,14 +243,7 @@ impl ValueCommitment { let rcv = generate_trapdoor(csprng)?; #[cfg(feature = "tx_v6")] - let vc = Self::new( - rcv, - // TODO: Make the `ValueSum::from_raw` function public in the `orchard` crate - // and use `ValueSum::from_raw(value.into())` instead of the next line. - // Remove `#[allow(clippy::unwrap_in_result)]` after doing so. - (ValueSum::default() + i64::from(value)).unwrap(), - AssetBase::native(), - ); + let vc = Self::new(rcv, ValueSum::from_raw(value.into()), AssetBase::zatoshi()); #[cfg(not(feature = "tx_v6"))] let vc = Self::new(rcv, value); diff --git a/zebra-chain/src/orchard/shielded_data.rs b/zebra-chain/src/orchard/shielded_data.rs index 2acf2c20519..43cde7e1db2 100644 --- a/zebra-chain/src/orchard/shielded_data.rs +++ b/zebra-chain/src/orchard/shielded_data.rs @@ -133,11 +133,8 @@ impl ShieldedData { let key = { let cv_balance = ValueCommitment::new( pallas::Scalar::zero(), - // TODO: Make the `ValueSum::from_raw` function public in the `orchard` crate - // and use `ValueSum::from_raw(self.value_balance.into())` instead of the - // next line - (ValueSum::default() + i64::from(self.value_balance)).unwrap(), - AssetBase::native(), + ValueSum::from_raw(self.value_balance.into()), + AssetBase::zatoshi(), ); let burn_value_commitment = compute_burn_value_commitment(self.burn.as_ref()); cv - cv_balance - burn_value_commitment diff --git a/zebra-chain/src/orchard/shielded_data_flavor.rs b/zebra-chain/src/orchard/shielded_data_flavor.rs index 11ef89df399..20a0b306643 100644 --- a/zebra-chain/src/orchard/shielded_data_flavor.rs +++ b/zebra-chain/src/orchard/shielded_data_flavor.rs @@ -4,12 +4,12 @@ use std::fmt::Debug; use serde::{de::DeserializeOwned, Serialize}; -use orchard::{orchard_flavor::OrchardFlavor, primitives::OrchardPrimitives}; +use orchard::{flavor::OrchardFlavor, primitives::OrchardPrimitives}; -pub use orchard::orchard_flavor::OrchardVanilla; +pub use orchard::flavor::OrchardVanilla; #[cfg(feature = "tx_v6")] -pub use orchard::{note::AssetBase, orchard_flavor::OrchardZSA, value::NoteValue}; +pub use orchard::{flavor::OrchardZSA, note::AssetBase, value::NoteValue}; use crate::serialization::{ZcashDeserialize, ZcashSerialize}; diff --git a/zebra-chain/src/orchard_zsa.rs b/zebra-chain/src/orchard_zsa.rs index a90c115a9b3..a8ea0f5924f 100644 --- a/zebra-chain/src/orchard_zsa.rs +++ b/zebra-chain/src/orchard_zsa.rs @@ -3,8 +3,16 @@ #[cfg(any(test, feature = "proptest-impl"))] mod arbitrary; +mod asset_state; mod burn; mod issuance; -pub(crate) use burn::{compute_burn_value_commitment, Burn, BurnItem, NoBurn}; +pub(crate) use burn::{compute_burn_value_commitment, Burn, NoBurn}; pub(crate) use issuance::IssueData; + +pub use burn::BurnItem; + +pub use asset_state::{AssetBase, AssetState, AssetStateError, IssuedAssetChanges}; + +#[cfg(any(test, feature = "proptest-impl"))] +pub use asset_state::testing::{mock_asset_base, mock_asset_state}; diff --git a/zebra-chain/src/orchard_zsa/arbitrary.rs b/zebra-chain/src/orchard_zsa/arbitrary.rs index 95091114260..7e68d40248b 100644 --- a/zebra-chain/src/orchard_zsa/arbitrary.rs +++ b/zebra-chain/src/orchard_zsa/arbitrary.rs @@ -15,7 +15,7 @@ impl Arbitrary for BurnItem { type Parameters = (); fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - BundleArb::::arb_asset_to_burn() + BundleArb::::arb_asset_to_burn() .prop_map(|(asset_base, value)| BurnItem::from((asset_base, value))) .boxed() } diff --git a/zebra-chain/src/orchard_zsa/asset_state.rs b/zebra-chain/src/orchard_zsa/asset_state.rs new file mode 100644 index 00000000000..5976d39e144 --- /dev/null +++ b/zebra-chain/src/orchard_zsa/asset_state.rs @@ -0,0 +1,430 @@ +//! Defines and implements the issued asset state types + +use byteorder::{ReadBytesExt, WriteBytesExt}; +use std::{ + collections::{BTreeMap, HashMap}, + io, + sync::Arc, +}; +use thiserror::Error; + +pub use orchard::note::AssetBase; +use orchard::{ + bundle::burn_validation::{validate_bundle_burn, BurnError}, + issuance::{ + check_issue_bundle_without_sighash, verify_issue_bundle, AssetRecord, Error as IssueError, + }, + note::Nullifier, + value::NoteValue, + Note, +}; + +use zcash_primitives::transaction::components::issuance::{read_note, write_note}; + +use crate::transaction::{SigHash, Transaction}; + +/// Wraps orchard's AssetRecord for use in zebra state management. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AssetState(AssetRecord); + +impl AssetState { + /// Creates a new [`AssetRecord`] instance. + pub fn new(amount: NoteValue, is_finalized: bool, reference_note: Note) -> Self { + Self(AssetRecord::new(amount, is_finalized, reference_note)) + } + + /// Deserializes a new [`AssetState`] from its canonical byte encoding. + pub fn from_bytes(bytes: &[u8]) -> Result { + use std::io::{Cursor, Read}; + + let mut reader = Cursor::new(bytes); + let mut amount_bytes = [0; 8]; + reader.read_exact(&mut amount_bytes)?; + + let is_finalized = match reader.read_u8()? { + 0 => false, + 1 => true, + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Invalid is_finalized", + )) + } + }; + + let mut asset_bytes = [0u8; 32]; + reader.read_exact(&mut asset_bytes)?; + let asset = Option::from(AssetBase::from_bytes(&asset_bytes)) + .ok_or(io::Error::new(io::ErrorKind::InvalidData, "Invalid asset"))?; + + let reference_note = read_note(reader, asset)?; + + Ok(AssetState(AssetRecord::new( + NoteValue::from_bytes(amount_bytes), + is_finalized, + reference_note, + ))) + } + + /// Serializes [`AssetState`] to its canonical byte encoding. + pub fn to_bytes(&self) -> Result, io::Error> { + use std::io::Write; + + // FIXME: Consider writing a leading version byte here so we can change AssetState's + // on-disk format without silently mis-parsing old DB entries during upgrades (and fix + // from_bytes accordingly). + let mut bytes = Vec::new(); + bytes.write_all(&self.0.amount.to_bytes())?; + bytes.write_u8(self.0.is_finalized as u8)?; + bytes.write_all(&self.0.reference_note.asset().to_bytes())?; + write_note(&mut bytes, &self.0.reference_note)?; + Ok(bytes) + } + + /// Returns whether the asset is finalized. + #[cfg(any(test, feature = "proptest-impl"))] + pub fn is_finalized(&self) -> bool { + self.0.is_finalized + } + + /// Returns the total supply. + #[cfg(any(test, feature = "proptest-impl"))] + pub fn total_supply(&self) -> u64 { + self.0.amount.inner() + } +} + +impl From for AssetState { + fn from(record: AssetRecord) -> Self { + Self(record) + } +} + +// Needed for the new `getassetstate` RPC endpoint in `zebra-rpc`. +// Can't derive `Serialize` here as `orchard::AssetRecord` doesn't implement it. +impl serde::Serialize for AssetState { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::{Error as _, SerializeStruct}; + + // "3" is the expected number of struct fields (a hint for pre-allocation). + let mut st = serializer.serialize_struct("AssetState", 3)?; + + let inner = &self.0; + st.serialize_field("amount", &inner.amount.inner())?; + st.serialize_field("is_finalized", &inner.is_finalized)?; + + let mut note_bytes = Vec::::new(); + write_note(&mut note_bytes, &inner.reference_note).map_err(S::Error::custom)?; + st.serialize_field("reference_note", &hex::encode(note_bytes))?; + + st.end() + } +} + +/// Errors returned when validating asset state updates. +#[derive(Debug, Error, Clone, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum AssetStateError { + #[error("issuance validation failed: {0}")] + Issue(IssueError), + + #[error("burn validation failed: {0}")] + Burn(BurnError), +} + +/// A map of asset state changes for assets modified in a block or transaction set. +/// Contains `(old_state, new_state)` pairs for each modified asset. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct IssuedAssetChanges(HashMap, AssetState)>); + +/// Apply validator output to the mutable state map. +fn apply_updates( + states: &mut HashMap, AssetState)>, + updates: BTreeMap, +) { + use std::collections::hash_map::Entry; + + for (asset, record) in updates { + match states.entry(asset) { + Entry::Occupied(mut entry) => entry.get_mut().1 = AssetState::from(record), + Entry::Vacant(entry) => { + entry.insert((None, AssetState::from(record))); + } + } + } +} + +impl IssuedAssetChanges { + /// Validates burns and issuances across transactions, returning the map of changes. + /// + /// # Signature Verification Modes + /// + /// - **With `transaction_sighashes` (Some)**: Full validation for Contextually Verified Blocks + /// from the consensus workflow. Performs signature verification using `verify_issue_bundle`. + /// + /// - **Without `transaction_sighashes` (None)**: Trusted validation for Checkpoint Verified Blocks + /// loaded during bootstrap/startup from disk. These blocks are within checkpoint ranges and + /// are considered trusted, so signature verification is skipped using `check_issue_bundle_without_sighash`. + #[allow(clippy::unwrap_in_result)] + pub fn validate_and_get_changes( + transactions: &[Arc], + transaction_sighashes: Option<&[SigHash]>, + get_state: impl Fn(&AssetBase) -> Option, + ) -> Result { + // When sighashes are provided, transactions and sighashes must be equal length by design, + // so we use assert instead of returning error. + if let Some(sighashes) = transaction_sighashes { + assert_eq!( + transactions.len(), + sighashes.len(), + "Bug in caller: {} transactions but {} sighashes. Caller must provide one sighash per transaction.", + transactions.len(), + sighashes.len() + ); + } + + // Track old and current states - old_state is None for newly created assets + let mut states = HashMap::, AssetState)>::new(); + + for (i, tx) in transactions.iter().enumerate() { + // Validate and apply burns + if let Some(burn) = tx.orchard_burns() { + let burn_records = validate_bundle_burn( + burn.iter() + .map(|burn_item| <(AssetBase, NoteValue)>::from(*burn_item)), + |asset| Self::get_or_cache_record(&mut states, asset, &get_state), + ) + .map_err(AssetStateError::Burn)?; + apply_updates(&mut states, burn_records); + } + + // Validate and apply issuances + if let Some(issue_data) = tx.orchard_issue_data() { + // ZIP-0227 defines issued-note rho as DeriveIssuedRho(nf_{0,0}, i_action, i_note), + // so we must pass the first Action nullifier (nf_{0,0}). We rely on + // `orchard_nullifiers()` preserving Action order, so `.next()` returns nf_{0,0}. + let first_nullifier = + // FIXME: For now, the only way to convert Zebra's nullifier type to Orchard's nullifier type + // is via bytes, although they both wrap pallas::Point. Consider a more direct conversion to + // avoid this round-trip, if possible. + &Nullifier::from_bytes(&<[u8; 32]>::from( + *tx.orchard_nullifiers() + .next() + // ZIP-0227 requires an issuance bundle to contain at least one OrchardZSA Action Group. + // `ShieldedData.actions` is `AtLeastOne<...>`, so nf_{0,0} must exist. + .expect("issuance must have at least one nullifier"), + )) + .expect("Bytes can be converted to Nullifier"); + + let issue_records = match transaction_sighashes { + Some(sighashes) => { + // Full verification with signature check (Contextually Verified Block) + verify_issue_bundle( + issue_data.inner(), + *sighashes[i].as_ref(), + |asset| Self::get_or_cache_record(&mut states, asset, &get_state), + first_nullifier, + ) + .map_err(AssetStateError::Issue)? + } + None => { + // Trusted verification without signature check (Checkpoint Verified Block) + check_issue_bundle_without_sighash( + issue_data.inner(), + |asset| Self::get_or_cache_record(&mut states, asset, &get_state), + first_nullifier, + ) + .map_err(AssetStateError::Issue)? + } + }; + + apply_updates(&mut states, issue_records); + } + } + + Ok(IssuedAssetChanges(states)) + } + + /// Gets current record from cache or fetches and caches it. + fn get_or_cache_record( + states: &mut HashMap, AssetState)>, + asset: &AssetBase, + get_state: &impl Fn(&AssetBase) -> Option, + ) -> Option { + use std::collections::hash_map::Entry; + + match states.entry(*asset) { + Entry::Occupied(entry) => Some(entry.get().1 .0), + Entry::Vacant(entry) => { + let state = get_state(asset)?; + entry.insert((Some(state), state)); + Some(state.0) + } + } + } + + /// Gets an iterator over `IssuedAssetChanges` inner `HashMap` elements. + pub fn iter(&self) -> impl Iterator, AssetState))> { + self.0.iter() + } +} + +impl From> for IssuedAssetChanges { + fn from(issued: HashMap) -> Self { + IssuedAssetChanges( + issued + .into_iter() + .map(|(base, state)| (base, (None, state))) + .collect(), + ) + } +} + +#[cfg(any(test, feature = "proptest-impl"))] +/// Test utilities for creating mock asset states and bases, used in zebra-rpc tests. +pub mod testing { + use super::AssetState; + + use orchard::{ + issuance::{ + auth::{IssueAuthKey, IssueValidatingKey, ZSASchnorr}, + compute_asset_desc_hash, IssueBundle, + }, + note::{AssetBase, AssetId, Nullifier}, + value::NoteValue, + }; + + use group::{ff::PrimeField, Curve, Group}; + use halo2::{arithmetic::CurveAffine, pasta::pallas}; + use rand::{RngCore, SeedableRng}; + use rand_chacha::ChaChaRng; + + const TEST_RNG_SEED: u64 = 0; + + fn hash_asset_desc(desc: &[u8]) -> [u8; 32] { + let (first, rest) = desc + .split_first() + .expect("asset description must be non-empty"); + compute_asset_desc_hash(&(*first, rest.to_vec()).into()) + } + + fn random_bytes(rng: &mut impl RngCore) -> [u8; N] { + let mut bytes = [0u8; N]; + rng.fill_bytes(&mut bytes); + bytes + } + + // Coordinate extractor for Pallas (nu5.pdf, § 5.4.9.7), used to create a nullifier. + fn extract_p(point: &pallas::Point) -> pallas::Base { + point + .to_affine() + .coordinates() + .map(|c| *c.x()) + .unwrap_or_else(pallas::Base::zero) + } + + fn dummy_nullifier(rng: impl RngCore) -> Nullifier { + Nullifier::from_bytes(&extract_p(&pallas::Point::random(rng)).to_repr()) + .expect("pallas x-coordinate is a valid nullifier") + } + + fn create_issue_keys( + rng: &mut (impl RngCore + rand::CryptoRng), + ) -> (IssueAuthKey, IssueValidatingKey) { + let isk = IssueAuthKey::::random(rng); + let ik = IssueValidatingKey::::from(&isk); + (isk, ik) + } + + // Creates a reference note whose `rho` is set, making it serializable via `AssetState::to_bytes`. + fn create_reference_note_with_rho( + asset_desc: &[u8], + rng: &mut (impl RngCore + rand::CryptoRng), + ) -> orchard::Note { + let (isk, ik) = create_issue_keys(&mut *rng); + let desc_hash = hash_asset_desc(asset_desc); + + let sighash = random_bytes::<32>(rng); + let first_nullifier = dummy_nullifier(&mut *rng); + let (bundle, _) = IssueBundle::new(ik, desc_hash, None, true, &mut *rng); + + let signed_bundle = bundle + .update_rho(&first_nullifier, rng) + .prepare(sighash) + .sign(&isk) + .expect("signing a freshly-created bundle must succeed"); + + signed_bundle + .actions() + .first() + .get_reference_note() + .expect("first action of IssueBundle always has a reference note") + .clone() + } + + /// Returns a deterministic [`AssetBase`] for the given description. + pub fn mock_asset_base(desc: &[u8]) -> AssetBase { + let mut rng = ChaChaRng::seed_from_u64(TEST_RNG_SEED); + let (_, ik) = create_issue_keys(&mut rng); + AssetBase::custom(&AssetId::new_v0(&ik, &hash_asset_desc(desc))) + } + + /// Returns a deterministic [`AssetState`] for use in tests. + pub fn mock_asset_state( + asset_desc: &[u8], + total_supply: u64, + is_finalized: bool, + ) -> AssetState { + let mut rng = ChaChaRng::seed_from_u64(TEST_RNG_SEED); + let reference_note = create_reference_note_with_rho(asset_desc, &mut rng); + AssetState::new( + NoteValue::from_bytes(total_supply.to_le_bytes()), + is_finalized, + reference_note, + ) + } +} + +#[cfg(test)] +mod tests { + use super::{testing::mock_asset_state, *}; + + #[test] + fn asset_state_roundtrip_serialization() { + let state = mock_asset_state(b"test_asset", 1000, false); + + let bytes = state.to_bytes().unwrap(); + let decoded = AssetState::from_bytes(&bytes).unwrap(); + + assert_eq!(state, decoded); + } + + #[test] + fn asset_state_finalized_roundtrip() { + let state = mock_asset_state(b"finalized", 5000, true); + + let bytes = state.to_bytes().unwrap(); + let decoded = AssetState::from_bytes(&bytes).unwrap(); + + assert!(decoded.is_finalized()); + assert_eq!(decoded.total_supply(), 5000); + } + + #[test] + fn read_asset_state_invalid_finalized_byte() { + let mut bytes = vec![0u8; 8]; // amount + bytes.push(2); // invalid is_finalized (not 0 or 1) + + let result = AssetState::from_bytes(&bytes); + assert!(result.is_err()); + } + + #[test] + fn issued_asset_changes_empty() { + let changes = IssuedAssetChanges::default(); + assert_eq!(changes.iter().count(), 0); + } +} diff --git a/zebra-chain/src/orchard_zsa/burn.rs b/zebra-chain/src/orchard_zsa/burn.rs index bc738c2a7ef..85250124de9 100644 --- a/zebra-chain/src/orchard_zsa/burn.rs +++ b/zebra-chain/src/orchard_zsa/burn.rs @@ -10,22 +10,9 @@ use zcash_primitives::transaction::components::orchard::{read_burn, write_burn}; use crate::{ orchard::ValueCommitment, - serialization::{ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize}, + serialization::{SerializationError, ZcashDeserialize, ZcashSerialize}, }; -impl ZcashSerialize for AssetBase { - fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { - writer.write_all(&self.to_bytes()) - } -} - -impl ZcashDeserialize for AssetBase { - fn zcash_deserialize(mut reader: R) -> Result { - Option::from(AssetBase::from_bytes(&reader.read_32_bytes()?)) - .ok_or_else(|| SerializationError::Parse("Invalid orchard_zsa AssetBase!")) - } -} - /// OrchardZSA burn item. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct BurnItem(AssetBase, NoteValue); @@ -171,7 +158,11 @@ impl ZcashDeserialize for Burn { pub(crate) fn compute_burn_value_commitment(burn: &[BurnItem]) -> ValueCommitment { burn.iter() .map(|&BurnItem(asset, amount)| { - ValueCommitment::new(pallas::Scalar::zero(), amount.into(), asset) + ValueCommitment::new( + pallas::Scalar::zero(), + amount - NoteValue::from_raw(0), + asset, + ) }) .sum() } diff --git a/zebra-chain/src/orchard_zsa/issuance.rs b/zebra-chain/src/orchard_zsa/issuance.rs index 856b64cccde..1c3f8b8614f 100644 --- a/zebra-chain/src/orchard_zsa/issuance.rs +++ b/zebra-chain/src/orchard_zsa/issuance.rs @@ -8,7 +8,7 @@ use group::ff::PrimeField; use halo2::pasta::pallas; use orchard::{ - issuance::{IssueBundle, Signed}, + issuance::{IssueAction, IssueBundle, Signed}, note::ExtractedNoteCommitment, }; @@ -47,6 +47,11 @@ impl IssueData { }) }) } + + /// Returns issuance actions + pub fn actions(&self) -> impl Iterator { + self.0.actions().iter() + } } impl ZcashSerialize for Option { diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 4f8762baeb3..e3a5532d510 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -1107,6 +1107,44 @@ impl Transaction { } } + /// Access the Orchard issue data in this transaction, if any, + /// regardless of version. + #[cfg(feature = "tx_v6")] + pub fn orchard_issue_data(&self) -> &Option { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { .. } => &None, + + Transaction::V6 { + orchard_zsa_issue_data, + .. + } => orchard_zsa_issue_data, + } + } + + /// Access the Orchard asset burns in this transaction, if there are any, + /// regardless of version. + #[cfg(feature = "tx_v6")] + pub fn orchard_burns(&self) -> Option<&'_ [orchard_zsa::BurnItem]> { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { .. } => None, + + Transaction::V6 { + orchard_shielded_data, + .. + } => orchard_shielded_data + .as_ref() + .map(|data| data.burn.as_ref()), + } + } + /// Access the [`orchard::Flags`] in this transaction, if there is any, /// regardless of version. pub fn orchard_flags(&self) -> Option { diff --git a/zebra-chain/src/transaction/tests/vectors.rs b/zebra-chain/src/transaction/tests/vectors.rs index 636813236c6..d39048d8da4 100644 --- a/zebra-chain/src/transaction/tests/vectors.rs +++ b/zebra-chain/src/transaction/tests/vectors.rs @@ -30,8 +30,10 @@ lazy_static! { sapling_shielded_data: None, orchard_shielded_data: None, }; +} - #[cfg(feature = "tx_v6")] +#[cfg(feature = "tx_v6")] +lazy_static! { pub static ref EMPTY_V6_TX: Transaction = Transaction::V6 { network_upgrade: NetworkUpgrade::Nu7, lock_time: LockTime::min_lock_time_timestamp(), @@ -491,8 +493,9 @@ fn v6_round_trip() { let _init_guard = zebra_test::init(); - for block_bytes in ORCHARD_ZSA_WORKFLOW_BLOCKS.iter() { - let block = block_bytes + for workflow_block in ORCHARD_ZSA_WORKFLOW_BLOCKS.iter() { + let block = workflow_block + .bytes .zcash_deserialize_into::() .expect("block is structurally valid"); @@ -502,7 +505,7 @@ fn v6_round_trip() { .expect("vec serialization is infallible"); assert_eq!( - block_bytes, &block_bytes2, + workflow_block.bytes, block_bytes2, "data must be equal if structs are equal" ); @@ -638,8 +641,9 @@ fn v6_librustzcash_tx_conversion() { let _init_guard = zebra_test::init(); - for block_bytes in ORCHARD_ZSA_WORKFLOW_BLOCKS.iter() { - let block = block_bytes + for workflow_block in ORCHARD_ZSA_WORKFLOW_BLOCKS.iter() { + let block = workflow_block + .bytes .zcash_deserialize_into::() .expect("block is structurally valid"); diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index 56495ba0526..7f6e35a7bb8 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -41,7 +41,7 @@ tx_v6 = [ ] [dependencies] -blake2b_simd = "1.0.2" +blake2b_simd = "=1.0.1" bellman = "0.14.0" bls12_381 = "0.8.0" halo2 = { package = "halo2_proofs", version = "0.3.0" } diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 611aea2ceba..e683e3191c0 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -8,6 +8,7 @@ //! verification, where it may be accepted or rejected. use std::{ + collections::HashMap, future::Future, pin::Pin, sync::Arc, @@ -25,6 +26,7 @@ use zebra_chain::{ amount::Amount, block, parameters::{subsidy::FundingStreamReceiver, Network}, + transaction::{SigHash, UnminedTxId}, transparent, work::equihash, }; @@ -253,25 +255,34 @@ where let mut legacy_sigop_count = 0; let mut block_miner_fees = Ok(Amount::zero()); + // Collect tx sighashes during verification and later emit them in block order (for ZSA issuance auth). + let mut tx_sighash_by_tx_id: HashMap = + HashMap::with_capacity(block.transactions.len()); + use futures::StreamExt; while let Some(result) = async_checks.next().await { tracing::trace!(?result, remaining = async_checks.len()); - let response = result + let crate::transaction::Response::Block { + tx_id, + miner_fee, + legacy_sigop_count: tx_legacy_sigop_count, + tx_sighash, + } = result .map_err(Into::into) - .map_err(VerifyBlockError::Transaction)?; - - assert!( - matches!(response, tx::Response::Block { .. }), - "unexpected response from transaction verifier: {response:?}" - ); + .map_err(VerifyBlockError::Transaction)? + else { + panic!("unexpected response from transaction verifier"); + }; - legacy_sigop_count += response.legacy_sigop_count(); + legacy_sigop_count += tx_legacy_sigop_count; // Coinbase transactions consume the miner fee, // so they don't add any value to the block's total miner fee. - if let Some(miner_fee) = response.miner_fee() { + if let Some(miner_fee) = miner_fee { block_miner_fees += miner_fee; } + + tx_sighash_by_tx_id.insert(tx_id, tx_sighash); } // Check the summed block totals @@ -314,12 +325,24 @@ where let new_outputs = Arc::into_inner(known_utxos) .expect("all verification tasks using known_utxos are complete"); + // Rebuild sighashes in block order to align with `block.transactions` indexing. + let transaction_sighashes: Arc<[SigHash]> = block + .transactions + .iter() + .map(|tx| { + *tx_sighash_by_tx_id + .get(&tx.unmined_id()) + .expect("every verified tx must return a sighash") + }) + .collect(); + let prepared_block = zs::SemanticallyVerifiedBlock { block, hash, height, new_outputs, transaction_hashes, + transaction_sighashes: Some(transaction_sighashes), deferred_balance: Some(expected_deferred_amount), }; diff --git a/zebra-consensus/src/orchard_zsa/tests.rs b/zebra-consensus/src/orchard_zsa/tests.rs index 4100c3be9c1..1444a09db7f 100644 --- a/zebra-consensus/src/orchard_zsa/tests.rs +++ b/zebra-consensus/src/orchard_zsa/tests.rs @@ -12,50 +12,268 @@ //! In short, it demonstrates end-to-end handling of Orchard asset burns and ZSA issuance through //! consensus (with state verification to follow in the next PR). -use std::sync::Arc; +use std::{ + collections::{hash_map, HashMap}, + sync::Arc, +}; use color_eyre::eyre::{eyre, Report}; use tokio::time::{timeout, Duration}; +use tower::ServiceExt; + +use orchard::{ + issuance::{ + auth::{IssueValidatingKey, ZSASchnorr}, + {AssetRecord, IssueAction}, + }, + note::{AssetBase, AssetId}, + value::NoteValue, +}; use zebra_chain::{ block::{genesis::regtest_genesis_block, Block, Hash}, + orchard_zsa::{AssetState, BurnItem}, parameters::Network, serialization::ZcashDeserialize, }; +use zebra_state::{ReadRequest, ReadResponse, ReadStateService}; + use zebra_test::{ transcript::{ExpectedTranscriptError, Transcript}, - vectors::ORCHARD_ZSA_WORKFLOW_BLOCKS, + vectors::{OrchardWorkflowBlock, ORCHARD_ZSA_WORKFLOW_BLOCKS}, }; use crate::{block::Request, Config}; -fn create_transcript_data() -> impl Iterator)> -{ - let workflow_blocks = ORCHARD_ZSA_WORKFLOW_BLOCKS.iter().map(|block_bytes| { - Arc::new(Block::zcash_deserialize(&block_bytes[..]).expect("block should deserialize")) - }); +type AssetRecords = HashMap; + +type TranscriptItem = (Request, Result); + +#[derive(Debug)] +enum AssetRecordsError { + BurnAssetMissing, + EmptyActionNotFinalized, + AmountOverflow, + MissingRefNote, + ModifyFinalized, +} + +/// Processes orchard burns, decreasing asset supply. +fn process_burns<'a, I: IntoIterator>( + asset_records: &mut AssetRecords, + burns: I, +) -> Result<(), AssetRecordsError> { + for burn in burns { + // FIXME: check for burn specific errors? + let asset_record = asset_records + .get_mut(&burn.asset()) + .ok_or(AssetRecordsError::BurnAssetMissing)?; + + asset_record.amount = NoteValue::from_raw( + asset_record + .amount + .inner() + .checked_sub(burn.amount().inner()) + .ok_or(AssetRecordsError::AmountOverflow)?, + ); + } + + Ok(()) +} + +/// Processes orchard issue actions, increasing asset supply. +fn process_issue_actions<'a, I: Iterator>( + asset_records: &mut AssetRecords, + ik: &IssueValidatingKey, + actions: I, +) -> Result<(), AssetRecordsError> { + for action in actions { + let action_asset = AssetBase::custom(&AssetId::new_v0(ik, action.asset_desc_hash())); + let reference_note = action.get_reference_note(); + let is_finalized = action.is_finalized(); + + let mut note_amounts = action.notes().iter().map(|note| { + if note.asset() == action_asset { + Ok(note.value()) + } else { + Err(AssetRecordsError::BurnAssetMissing) + } + }); + + let first_note_amount = match note_amounts.next() { + Some(note_amount) => note_amount, + None => { + if is_finalized { + Ok(NoteValue::from_raw(0)) + } else { + Err(AssetRecordsError::EmptyActionNotFinalized) + } + } + }; + + for amount_result in std::iter::once(first_note_amount).chain(note_amounts) { + let amount = amount_result?; + + // FIXME: check for issuance specific errors? + match asset_records.entry(action_asset) { + hash_map::Entry::Occupied(mut entry) => { + let asset_record = entry.get_mut(); + asset_record.amount = asset_record + .amount + .inner() + .checked_add(amount.inner()) + .map(|raw| NoteValue::from_raw(raw)) + .ok_or(AssetRecordsError::AmountOverflow)?; + if asset_record.is_finalized { + return Err(AssetRecordsError::ModifyFinalized); + } + asset_record.is_finalized = is_finalized; + } + + hash_map::Entry::Vacant(entry) => { + entry.insert(AssetRecord { + amount, + is_finalized, + reference_note: *reference_note.ok_or(AssetRecordsError::MissingRefNote)?, + }); + } + } + } + } - std::iter::once(regtest_genesis_block()) + Ok(()) +} + +/// Builds assets records for the given blocks. +fn build_asset_records<'a, I: IntoIterator>( + blocks: I, +) -> Result { + blocks + .into_iter() + .filter_map(|(request, result)| match (request, result) { + (Request::Commit(block), Ok(_)) => Some(&block.transactions), + _ => None, + }) + .flatten() + .try_fold(HashMap::new(), |mut asset_records, tx| { + if let Some(burns) = tx.orchard_burns() { + process_burns(&mut asset_records, burns.iter())?; + } + + if let Some(issue_data) = tx.orchard_issue_data() { + process_issue_actions( + &mut asset_records, + issue_data.inner().ik(), + issue_data.actions(), + )?; + } + + Ok(asset_records) + }) +} + +/// Creates transcript data from predefined workflow blocks. +fn create_transcript_data<'a, I: IntoIterator>( + serialized_blocks: I, +) -> impl Iterator + use<'a, I> { + let workflow_blocks = + serialized_blocks + .into_iter() + .map(|OrchardWorkflowBlock { bytes, is_valid }| { + ( + Arc::new( + Block::zcash_deserialize(&bytes[..]).expect("block should deserialize"), + ), + *is_valid, + ) + }); + + std::iter::once((regtest_genesis_block(), true)) .chain(workflow_blocks) - .map(|block| (Request::Commit(block.clone()), Ok(block.hash()))) + .map(|(block, is_valid)| { + ( + Request::Commit(block.clone()), + if is_valid { + Ok(block.hash()) + } else { + Err(ExpectedTranscriptError::Any) + }, + ) + }) +} + +/// Queries the state service for the asset state of the given asset. +async fn request_asset_state( + read_state_service: &ReadStateService, + asset_base: AssetBase, +) -> Option { + let request = ReadRequest::AssetState { + asset_base, + include_non_finalized: true, + }; + + match read_state_service.clone().oneshot(request).await { + Ok(ReadResponse::AssetState(asset_state)) => asset_state, + _ => unreachable!("The state service returned an unexpected response."), + } } #[tokio::test(flavor = "multi_thread")] -async fn check_zsa_workflow() -> Result<(), Report> { +async fn check_orchard_zsa_workflow() -> Result<(), Report> { let _init_guard = zebra_test::init(); let network = Network::new_regtest(Some(1), Some(1), Some(1)); - let state_service = zebra_state::init_test(&network); + let (state_service, read_state_service, _, _) = zebra_state::init_test_services(&network); let (block_verifier_router, ..) = crate::router::init(Config::default(), &network, state_service).await; + let transcript_data = + create_transcript_data(ORCHARD_ZSA_WORKFLOW_BLOCKS.iter()).collect::>(); + + let asset_records = + build_asset_records(&transcript_data).expect("should calculate asset_records"); + + // Before applying the blocks, ensure that none of the assets exist in the state. + for &asset_base in asset_records.keys() { + assert!( + request_asset_state(&read_state_service, asset_base) + .await + .is_none(), + "State should initially have no info about this asset." + ); + } + + // Verify all blocks in the transcript against the consensus and the state. timeout( Duration::from_secs(15), - Transcript::from(create_transcript_data()).check(block_verifier_router), + Transcript::from(transcript_data).check(block_verifier_router), ) .await - .map_err(|_| eyre!("Task timed out"))? + .map_err(|_| eyre!("Task timed out"))??; + + // After processing the transcript blocks, verify that the state matches the expected supply info. + for (&asset_base, asset_record) in &asset_records { + let asset_state = request_asset_state(&read_state_service, asset_base) + .await + .expect("State should contain this asset now."); + + assert_eq!( + asset_state.is_finalized(), + asset_record.is_finalized, + "Finalized state does not match for asset {:?}.", + asset_base + ); + + assert_eq!( + asset_state.total_supply(), + asset_record.amount.inner(), + "Total supply mismatch for asset {:?}.", + asset_base + ); + } + + Ok(()) } diff --git a/zebra-consensus/src/primitives/halo2/tests.rs b/zebra-consensus/src/primitives/halo2/tests.rs index 1b2da85af00..f3ec0763c8a 100644 --- a/zebra-consensus/src/primitives/halo2/tests.rs +++ b/zebra-consensus/src/primitives/halo2/tests.rs @@ -62,13 +62,17 @@ where recipient, NoteValue::from_raw(note_value), // FIXME: Use another AssetBase for OrchardZSA? - AssetBase::native(), + AssetBase::zatoshi(), memo, ) .unwrap(); } - let bundle: Bundle<_, i64, Flavor> = builder.build(rng).unwrap().0; + let bundle: Bundle<_, i64, Flavor> = builder + .build(rng) + .unwrap() + .expect("Bundle should not be None") + .0; let bundle = bundle .create_proof(&proving_key, rng) diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index 4e134f7f59e..ec629ce7537 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -143,6 +143,9 @@ pub enum Response { /// The number of legacy signature operations in this transaction's /// transparent inputs and outputs. legacy_sigop_count: u64, + + /// Shielded sighash for this transaction. + tx_sighash: SigHash, }, /// A response to a mempool transaction verification request. @@ -384,7 +387,7 @@ where tracing::trace!(?tx_id, "got state UTXOs"); - let mut async_checks = match tx.as_ref() { + let (mut async_checks, tx_sighash) = match tx.as_ref() { Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => { tracing::debug!(?tx, "got transaction with wrong version"); return Err(TransactionError::WrongVersion); @@ -478,6 +481,7 @@ where tx_id, miner_fee, legacy_sigop_count, + tx_sighash }, Request::Mempool { transaction, .. } => { let transaction = VerifiedUnminedTx::new( @@ -655,7 +659,7 @@ where cached_ffi_transaction: Arc, joinsplit_data: &Option>, sapling_shielded_data: &Option>, - ) -> Result { + ) -> Result<(AsyncChecks, SigHash), TransactionError> { let tx = request.transaction(); let upgrade = request.upgrade(network); @@ -670,7 +674,7 @@ where None, ); - Ok(Self::verify_transparent_inputs_and_outputs( + let async_check = Self::verify_transparent_inputs_and_outputs( request, network, script_verifier, @@ -683,7 +687,9 @@ where .and(Self::verify_sapling_shielded_data( sapling_shielded_data, &shielded_sighash, - )?)) + )?); + + Ok((async_check, shielded_sighash)) } /// Verifies if a V4 `transaction` is supported by `network_upgrade`. @@ -753,7 +759,7 @@ where cached_ffi_transaction: Arc, sapling_shielded_data: &Option>, orchard_shielded_data: &Option>, - ) -> Result { + ) -> Result<(AsyncChecks, SigHash), TransactionError> { let transaction = request.transaction(); let upgrade = request.upgrade(network); @@ -768,7 +774,7 @@ where None, ); - Ok(Self::verify_transparent_inputs_and_outputs( + let async_check = Self::verify_transparent_inputs_and_outputs( request, network, script_verifier, @@ -781,7 +787,9 @@ where .and(Self::verify_orchard_shielded_data( orchard_shielded_data, &shielded_sighash, - )?)) + )?); + + Ok((async_check, shielded_sighash)) } /// Verifies if a V5/V6 `transaction` is supported by `network_upgrade`. diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 8becc5bb79c..5c7df0991cb 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -302,6 +302,17 @@ pub trait Rpc { address_strings: AddressStrings, ) -> BoxFuture>>; + /// Returns the asset state of the provided asset base at the best chain tip or finalized chain tip. + /// + /// method: post + /// tags: blockchain + #[rpc(name = "getassetstate")] + fn get_asset_state( + &self, + asset_base: String, + include_non_finalized: Option, + ) -> BoxFuture>; + /// Stop the running zebrad process. /// /// # Notes @@ -1358,6 +1369,40 @@ where .boxed() } + // FIXME: Add #[cfg(feature = "tx_v6")] here + fn get_asset_state( + &self, + asset_base: String, + include_non_finalized: Option, + ) -> BoxFuture> { + let state = self.state.clone(); + let include_non_finalized = include_non_finalized.unwrap_or(true); + + async move { + let asset_base = zebra_chain::orchard_zsa::AssetBase::from_bytes( + &hex::decode(asset_base).map_server_error()?[..] + .try_into() + .map_server_error()?, + ) + .into_option() + .ok_or_server_error("invalid asset base")?; + + let request = zebra_state::ReadRequest::AssetState { + asset_base, + include_non_finalized, + }; + + let zebra_state::ReadResponse::AssetState(asset_state) = + state.oneshot(request).await.map_server_error()? + else { + unreachable!("unexpected response from state service"); + }; + + asset_state.ok_or_server_error("asset base not found") + } + .boxed() + } + fn stop(&self) -> Result { #[cfg(not(target_os = "windows"))] if self.network.is_regtest() { diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index f4d7804088e..982325e4f67 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -14,6 +14,7 @@ use zebra_chain::{ block::Block, chain_tip::mock::MockChainTip, orchard, + orchard_zsa::{mock_asset_base, mock_asset_state}, parameters::{ subsidy::POST_NU6_FUNDING_STREAMS_TESTNET, testnet::{self, ConfiguredActivationHeights, Parameters}, @@ -536,6 +537,39 @@ async fn test_mocked_rpc_response_data_for_network(network: &Network) { settings.bind(|| { insta::assert_json_snapshot!(format!("z_get_subtrees_by_index_for_orchard"), subtrees) }); + + // Test the response format from `getassetstate`. + + // Prepare the state response and make the RPC request. + let asset_base = mock_asset_base(b"Asset1"); + let rsp = state + .expect_request_that(|req| matches!(req, ReadRequest::AssetState { .. })) + .map(|responder| responder.respond(ReadResponse::AssetState(None))); + let req = rpc.get_asset_state(hex::encode(asset_base.to_bytes()), None); + + // Get the RPC error response. + let (asset_state_rsp, ..) = tokio::join!(req, rsp); + let asset_state = asset_state_rsp.expect_err("The RPC response should be an error"); + + // Check the error response. + settings + .bind(|| insta::assert_json_snapshot!(format!("get_asset_state_not_found"), asset_state)); + + // Prepare the state response and make the RPC request. + let asset_base = mock_asset_base(b"Asset2"); + let asset_state = mock_asset_state(b"Asset2", 1000, true); + let rsp = state + .expect_request_that(|req| matches!(req, ReadRequest::AssetState { .. })) + .map(|responder| responder.respond(ReadResponse::AssetState(Some(asset_state)))); + let req = rpc.get_asset_state(hex::encode(asset_base.to_bytes()), None); + + // Get the RPC response. + let (asset_state_rsp, ..) = tokio::join!(req, rsp); + let asset_state = + asset_state_rsp.expect("The RPC response should contain a `AssetState` struct."); + + // Check the response. + settings.bind(|| insta::assert_json_snapshot!(format!("get_asset_state"), asset_state)); } /// Snapshot `getinfo` response, using `cargo insta` and JSON serialization. diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state@mainnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state@mainnet.snap new file mode 100644 index 00000000000..22a4d89f6fc --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state@mainnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: asset_state +--- +{ + "amount": 1000, + "is_finalized": true, + "reference_note": "cc36601959213b6b0cdb96a75c17c3a668a97f0d6a8c5ce164a518ea9ba9a50ea75191fd861b0ff10e62b000000000000000000000000000000000000000000000000000000000000000000000000000000000b2f7f581d6de3c06a822fd6e7e8265fbc00f8401696a5bdc34f5a6d2ff3f922f" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state@testnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state@testnet.snap new file mode 100644 index 00000000000..22a4d89f6fc --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state@testnet.snap @@ -0,0 +1,9 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: asset_state +--- +{ + "amount": 1000, + "is_finalized": true, + "reference_note": "cc36601959213b6b0cdb96a75c17c3a668a97f0d6a8c5ce164a518ea9ba9a50ea75191fd861b0ff10e62b000000000000000000000000000000000000000000000000000000000000000000000000000000000b2f7f581d6de3c06a822fd6e7e8265fbc00f8401696a5bdc34f5a6d2ff3f922f" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@mainnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@mainnet.snap new file mode 100644 index 00000000000..9efcfd5868f --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@mainnet.snap @@ -0,0 +1,8 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: asset_state +--- +{ + "code": 0, + "message": "asset base not found" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@testnet.snap b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@testnet.snap new file mode 100644 index 00000000000..9efcfd5868f --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_asset_state_not_found@testnet.snap @@ -0,0 +1,8 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: asset_state +--- +{ + "code": 0, + "message": "asset base not found" +} diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index 5c0b837566a..43d4309a5d4 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -37,6 +37,8 @@ impl Prepare for Arc { height, new_outputs, transaction_hashes, + // FIXME: Do we need to (and can we) generate real arbitrary transaction_sighashes? + transaction_sighashes: None, deferred_balance: None, } } @@ -96,8 +98,13 @@ impl ContextuallyVerifiedBlock { .map(|outpoint| (outpoint, zero_utxo.clone())) .collect(); - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, zero_spent_utxos) - .expect("all UTXOs are provided with zero values") + ContextuallyVerifiedBlock::with_block_and_spent_utxos( + block, + zero_spent_utxos, + #[cfg(feature = "tx_v6")] + Default::default(), + ) + .expect("all UTXOs are provided with zero values") } /// Create a [`ContextuallyVerifiedBlock`] from a [`Block`] or [`SemanticallyVerifiedBlock`], @@ -111,6 +118,7 @@ impl ContextuallyVerifiedBlock { height, new_outputs, transaction_hashes, + transaction_sighashes, deferred_balance: _, } = block.into(); @@ -124,7 +132,10 @@ impl ContextuallyVerifiedBlock { // TODO: fix the tests, and stop adding unrelated inputs and outputs. spent_outputs: new_outputs, transaction_hashes, + transaction_sighashes, chain_value_pool_change: ValueBalance::zero(), + #[cfg(feature = "tx_v6")] + issued_asset_changes: Default::default(), } } } diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index cf495311efb..1149716177d 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -9,7 +9,7 @@ use zebra_chain::{ amount::{self, NegativeAllowed, NonNegative}, block, history_tree::HistoryTreeError, - orchard, sapling, sprout, transaction, transparent, + orchard, orchard_zsa, sapling, sprout, transaction, transparent, value_balance::{ValueBalance, ValueBalanceError}, work::difficulty::CompactDifficulty, }; @@ -264,6 +264,9 @@ pub enum ValidateContextError { tx_index_in_block: Option, transaction_hash: transaction::Hash, }, + + #[error("error updating issued asset state")] + InvalidIssuedAsset(#[from] orchard_zsa::AssetStateError), } /// Trait for creating the corresponding duplicate nullifier error from a nullifier. diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 56be011d48e..e72f2519c29 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -21,6 +21,9 @@ use zebra_chain::{ value_balance::{ValueBalance, ValueBalanceError}, }; +#[cfg(feature = "tx_v6")] +use zebra_chain::orchard_zsa::{AssetBase, IssuedAssetChanges}; + /// Allow *only* these unused imports, so that rustdoc link resolution /// will work with inline links. #[allow(unused_imports)] @@ -161,6 +164,9 @@ pub struct SemanticallyVerifiedBlock { /// A precomputed list of the hashes of the transactions in this block, /// in the same order as `block.transactions`. pub transaction_hashes: Arc<[transaction::Hash]>, + /// A precomputed list of the sighashes of the transactions in this block, + /// in the same order as `block.transactions`. + pub transaction_sighashes: Option>, /// This block's contribution to the deferred pool. pub deferred_balance: Option>, } @@ -221,8 +227,19 @@ pub struct ContextuallyVerifiedBlock { /// in the same order as `block.transactions`. pub(crate) transaction_hashes: Arc<[transaction::Hash]>, + /// A precomputed list of the sighashes of the transactions in this block, + /// in the same order as `block.transactions`. + pub transaction_sighashes: Option>, + /// The sum of the chain value pool changes of all transactions in this block. pub(crate) chain_value_pool_change: ValueBalance, + + #[cfg(feature = "tx_v6")] + /// Asset state changes for assets modified in this block. + /// Maps asset_base -> (old_state, new_state) where: + /// - old_state: the state before this block was applied + /// - new_state: the state after this block was applied + pub(crate) issued_asset_changes: IssuedAssetChanges, } /// Wraps note commitment trees and the history tree together. @@ -293,12 +310,22 @@ pub struct FinalizedBlock { pub(super) treestate: Treestate, /// This block's contribution to the deferred pool. pub(super) deferred_balance: Option>, + #[cfg(feature = "tx_v6")] + /// Asset state changes to be applied to the finalized state. + /// Contains (old_state, new_state) pairs for assets modified in this block. + /// If `None`, the changes will be recalculated from the block's transactions. + pub issued_asset_changes: Option, } impl FinalizedBlock { /// Constructs [`FinalizedBlock`] from [`CheckpointVerifiedBlock`] and its [`Treestate`]. pub fn from_checkpoint_verified(block: CheckpointVerifiedBlock, treestate: Treestate) -> Self { - Self::from_semantically_verified(SemanticallyVerifiedBlock::from(block), treestate) + Self::from_semantically_verified( + SemanticallyVerifiedBlock::from(block), + treestate, + #[cfg(feature = "tx_v6")] + None, + ) } /// Constructs [`FinalizedBlock`] from [`ContextuallyVerifiedBlock`] and its [`Treestate`]. @@ -306,11 +333,22 @@ impl FinalizedBlock { block: ContextuallyVerifiedBlock, treestate: Treestate, ) -> Self { - Self::from_semantically_verified(SemanticallyVerifiedBlock::from(block), treestate) + #[cfg(feature = "tx_v6")] + let issued_asset_changes = Some(block.issued_asset_changes.clone()); + Self::from_semantically_verified( + SemanticallyVerifiedBlock::from(block), + treestate, + #[cfg(feature = "tx_v6")] + issued_asset_changes, + ) } /// Constructs [`FinalizedBlock`] from [`SemanticallyVerifiedBlock`] and its [`Treestate`]. - fn from_semantically_verified(block: SemanticallyVerifiedBlock, treestate: Treestate) -> Self { + fn from_semantically_verified( + block: SemanticallyVerifiedBlock, + treestate: Treestate, + #[cfg(feature = "tx_v6")] issued_asset_changes: Option, + ) -> Self { Self { block: block.block, hash: block.hash, @@ -319,6 +357,8 @@ impl FinalizedBlock { transaction_hashes: block.transaction_hashes, treestate, deferred_balance: block.deferred_balance, + #[cfg(feature = "tx_v6")] + issued_asset_changes, } } } @@ -384,6 +424,7 @@ impl ContextuallyVerifiedBlock { pub fn with_block_and_spent_utxos( semantically_verified: SemanticallyVerifiedBlock, mut spent_outputs: HashMap, + #[cfg(feature = "tx_v6")] issued_asset_changes: IssuedAssetChanges, ) -> Result { let SemanticallyVerifiedBlock { block, @@ -391,6 +432,7 @@ impl ContextuallyVerifiedBlock { height, new_outputs, transaction_hashes, + transaction_sighashes, deferred_balance, } = semantically_verified; @@ -407,10 +449,13 @@ impl ContextuallyVerifiedBlock { new_outputs, spent_outputs: spent_outputs.clone(), transaction_hashes, + transaction_sighashes, chain_value_pool_change: block.chain_value_pool_change( &utxos_from_ordered_utxos(spent_outputs), deferred_balance, )?, + #[cfg(feature = "tx_v6")] + issued_asset_changes, }) } } @@ -427,6 +472,7 @@ impl CheckpointVerifiedBlock { block.deferred_balance = deferred_balance; block } + /// Creates a block that's ready to be committed to the finalized state, /// using a precalculated [`block::Hash`]. /// @@ -452,6 +498,9 @@ impl SemanticallyVerifiedBlock { height, new_outputs, transaction_hashes, + // Not used in checkpoint paths. + // FIXME: Is this correct? + transaction_sighashes: None, deferred_balance: None, } } @@ -465,7 +514,7 @@ impl SemanticallyVerifiedBlock { impl From> for CheckpointVerifiedBlock { fn from(block: Arc) -> Self { - CheckpointVerifiedBlock(SemanticallyVerifiedBlock::from(block)) + Self(SemanticallyVerifiedBlock::from(block)) } } @@ -484,6 +533,9 @@ impl From> for SemanticallyVerifiedBlock { height, new_outputs, transaction_hashes, + // Not used in checkpoint paths. + // FIXME: Is this correct? + transaction_sighashes: None, deferred_balance: None, } } @@ -497,6 +549,7 @@ impl From for SemanticallyVerifiedBlock { height: valid.height, new_outputs: valid.new_outputs, transaction_hashes: valid.transaction_hashes, + transaction_sighashes: valid.transaction_sighashes, deferred_balance: Some( valid .chain_value_pool_change @@ -508,19 +561,6 @@ impl From for SemanticallyVerifiedBlock { } } -impl From for SemanticallyVerifiedBlock { - fn from(finalized: FinalizedBlock) -> Self { - Self { - block: finalized.block, - hash: finalized.hash, - height: finalized.height, - new_outputs: finalized.new_outputs, - transaction_hashes: finalized.transaction_hashes, - deferred_balance: finalized.deferred_balance, - } - } -} - impl From for SemanticallyVerifiedBlock { fn from(checkpoint_verified: CheckpointVerifiedBlock) -> Self { checkpoint_verified.0 @@ -1068,6 +1108,17 @@ pub enum ReadRequest { /// Returns [`ReadResponse::TipBlockSize(usize)`](ReadResponse::TipBlockSize) /// with the current best chain tip block size in bytes. TipBlockSize, + + #[cfg(feature = "tx_v6")] + /// Returns [`ReadResponse::AssetState`] with an [`AssetState`](zebra_chain::orchard_zsa::AssetState) + /// of the provided [`AssetBase`] if it exists for the best chain tip or finalized chain tip (depending + /// on the `include_non_finalized` flag). + AssetState { + /// The [`AssetBase`] to return the asset state for. + asset_base: AssetBase, + /// Whether to include the issued asset state changes in the non-finalized state. + include_non_finalized: bool, + }, } impl ReadRequest { @@ -1105,6 +1156,8 @@ impl ReadRequest { ReadRequest::CheckBlockProposalValidity(_) => "check_block_proposal_validity", #[cfg(feature = "getblocktemplate-rpcs")] ReadRequest::TipBlockSize => "tip_block_size", + #[cfg(feature = "tx_v6")] + ReadRequest::AssetState { .. } => "asset_state", } } diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 77c252b0c75..4db9f3e2e84 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -13,6 +13,9 @@ use zebra_chain::{ value_balance::ValueBalance, }; +#[cfg(feature = "tx_v6")] +use zebra_chain::orchard_zsa::AssetState; + #[cfg(feature = "getblocktemplate-rpcs")] use zebra_chain::work::difficulty::CompactDifficulty; @@ -125,6 +128,7 @@ impl MinedTx { #[derive(Clone, Debug, PartialEq, Eq)] /// A response to a read-only /// [`ReadStateService`](crate::service::ReadStateService)'s [`ReadRequest`]. +#[allow(clippy::large_enum_variant)] pub enum ReadResponse { /// Response to [`ReadRequest::Tip`] with the current best chain tip. Tip(Option<(block::Height, block::Hash)>), @@ -233,6 +237,10 @@ pub enum ReadResponse { #[cfg(feature = "getblocktemplate-rpcs")] /// Response to [`ReadRequest::TipBlockSize`] TipBlockSize(Option), + + #[cfg(feature = "tx_v6")] + /// Response to [`ReadRequest::AssetState`] + AssetState(Option), } /// A structure with the information needed from the state to build a `getblocktemplate` RPC response. @@ -322,6 +330,9 @@ impl TryFrom for Response { ReadResponse::ChainInfo(_) | ReadResponse::SolutionRate(_) | ReadResponse::TipBlockSize(_) => { Err("there is no corresponding Response for this ReadResponse") } + + #[cfg(feature = "tx_v6")] + ReadResponse::AssetState(_) => Err("there is no corresponding Response for this ReadResponse"), } } } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index adc61f887ae..c4a593a160c 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1947,6 +1947,30 @@ impl Service for ReadStateService { }) .wait_for_panics() } + + #[cfg(feature = "tx_v6")] + ReadRequest::AssetState { + asset_base, + include_non_finalized, + } => { + let state = self.clone(); + + tokio::task::spawn_blocking(move || { + span.in_scope(move || { + let best_chain = include_non_finalized + .then(|| state.latest_best_chain()) + .flatten(); + + let response = read::asset_state(best_chain, &state.db, &asset_base); + + // The work is done in the future. + timer.finish(module_path!(), line!(), "ReadRequest::AssetState"); + + Ok(ReadResponse::AssetState(response)) + }) + }) + .wait_for_panics() + } } } } diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 04ea61d6982..c79788fbc36 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -115,6 +115,8 @@ impl From for ChainTipBlock { height, new_outputs: _, transaction_hashes, + // FIXME: Is it correct not to use sighashes here? Should we add transaction_sighashes to ChainTipBlock? + transaction_sighashes: _, deferred_balance: _, } = prepared; diff --git a/zebra-state/src/service/check/tests.rs b/zebra-state/src/service/check/tests.rs index 9608105766d..e82e9be681e 100644 --- a/zebra-state/src/service/check/tests.rs +++ b/zebra-state/src/service/check/tests.rs @@ -1,6 +1,7 @@ //! Tests for state contextual validation checks. mod anchors; +mod issuance; mod nullifier; mod utxo; mod vectors; diff --git a/zebra-state/src/service/check/tests/issuance.rs b/zebra-state/src/service/check/tests/issuance.rs new file mode 100644 index 00000000000..3a871eee65f --- /dev/null +++ b/zebra-state/src/service/check/tests/issuance.rs @@ -0,0 +1,100 @@ +use std::sync::Arc; + +use zebra_chain::{ + block::{self, genesis::regtest_genesis_block, Block}, + orchard_zsa::{AssetBase, IssuedAssetChanges}, + parameters::Network, + serialization::ZcashDeserialize, +}; + +use zebra_test::vectors::{OrchardWorkflowBlock, ORCHARD_ZSA_WORKFLOW_BLOCKS}; + +use crate::{ + check::Chain, + service::{finalized_state::FinalizedState, read, write::validate_and_commit_non_finalized}, + CheckpointVerifiedBlock, Config, NonFinalizedState, +}; + +fn valid_issuance_blocks() -> Vec> { + ORCHARD_ZSA_WORKFLOW_BLOCKS + .iter() + .map(|OrchardWorkflowBlock { bytes, .. }| { + Arc::new(Block::zcash_deserialize(&bytes[..]).expect("block should deserialize")) + }) + .collect() +} + +#[test] +fn check_burns_and_issuance() { + let _init_guard = zebra_test::init(); + + let network = Network::new_regtest(Some(1), None, Some(1)); + + let mut finalized_state = FinalizedState::new_with_debug( + &Config::ephemeral(), + &network, + true, + #[cfg(feature = "elasticsearch")] + false, + false, + ); + + let mut non_finalized_state = NonFinalizedState::new(&network); + + let regtest_genesis_block = regtest_genesis_block(); + let regtest_genesis_hash = regtest_genesis_block.hash(); + + finalized_state + .commit_finalized_direct(regtest_genesis_block.into(), None, "test") + .expect("unexpected invalid genesis block test vector"); + + let block = valid_issuance_blocks().first().unwrap().clone(); + let mut header = Arc::::unwrap_or_clone(block.header.clone()); + header.previous_block_hash = regtest_genesis_hash; + header.commitment_bytes = [0; 32].into(); + let block = Arc::new(Block { + header: Arc::new(header), + transactions: block.transactions.clone(), + }); + + let CheckpointVerifiedBlock(block) = CheckpointVerifiedBlock::new(block, None, None); + + let empty_chain = Chain::new( + &network, + finalized_state + .db + .finalized_tip_height() + .unwrap_or(block::Height::MIN), + finalized_state.db.sprout_tree_for_tip(), + finalized_state.db.sapling_tree_for_tip(), + finalized_state.db.orchard_tree_for_tip(), + finalized_state.db.history_tree(), + finalized_state.db.finalized_value_pool(), + ); + + let block_1_issued_assets = IssuedAssetChanges::validate_and_get_changes( + &block.block.transactions, + None, + |asset_base: &AssetBase| { + read::asset_state( + Some(&Arc::new(empty_chain.clone())), + &finalized_state.db, + asset_base, + ) + }, + ) + .expect("test transactions should be valid"); + + validate_and_commit_non_finalized(&finalized_state.db, &mut non_finalized_state, block) + .expect("validation should succeed"); + + let best_chain = non_finalized_state + .best_chain() + .expect("should have a non-finalized chain"); + + assert_eq!( + IssuedAssetChanges::from(best_chain.issued_assets.clone()), + block_1_issued_assets, + "issued assets for chain should match those of block 1" + ); +} diff --git a/zebra-state/src/service/finalized_state.rs b/zebra-state/src/service/finalized_state.rs index f8c9bade5c1..94328d9e51f 100644 --- a/zebra-state/src/service/finalized_state.rs +++ b/zebra-state/src/service/finalized_state.rs @@ -91,6 +91,7 @@ pub const STATE_COLUMN_FAMILIES_IN_CODE: &[&str] = &[ "orchard_anchors", "orchard_note_commitment_tree", "orchard_note_commitment_subtree", + "orchard_issued_assets", // Chain "history_tree", "tip_chain_value_pool", diff --git a/zebra-state/src/service/finalized_state/disk_format/shielded.rs b/zebra-state/src/service/finalized_state/disk_format/shielded.rs index bcd24d5c604..9cd23047d0c 100644 --- a/zebra-state/src/service/finalized_state/disk_format/shielded.rs +++ b/zebra-state/src/service/finalized_state/disk_format/shielded.rs @@ -13,6 +13,9 @@ use zebra_chain::{ subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, }; +#[cfg(feature = "tx_v6")] +use zebra_chain::orchard_zsa::{AssetBase, AssetState}; + use crate::service::finalized_state::disk_format::{FromDisk, IntoDisk}; use super::block::HEIGHT_DISK_BYTES; @@ -207,3 +210,43 @@ impl FromDisk for NoteCommitmentSubtreeData { ) } } + +// TODO: Replace `.unwrap()`s with `.expect()`s + +#[cfg(feature = "tx_v6")] +impl IntoDisk for AssetState { + type Bytes = Vec; + + fn as_bytes(&self) -> Self::Bytes { + self.to_bytes() + .expect("asset state should serialize successfully") + } +} + +#[cfg(feature = "tx_v6")] +impl FromDisk for AssetState { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + Self::from_bytes(bytes.as_ref()).expect("asset state should deserialize successfully") + } +} + +#[cfg(feature = "tx_v6")] +impl IntoDisk for AssetBase { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.to_bytes() + } +} + +#[cfg(feature = "tx_v6")] +impl FromDisk for AssetBase { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + bytes + .as_ref() + .try_into() + .ok() + .and_then(|asset_bytes| Option::from(Self::from_bytes(asset_bytes))) + .expect("asset base should deserialize successfully") + } +} diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap index d37e037cac7..33f1c76717b 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap @@ -12,6 +12,7 @@ expression: cf_names "height_by_hash", "history_tree", "orchard_anchors", + "orchard_issued_assets", "orchard_note_commitment_subtree", "orchard_note_commitment_tree", "orchard_nullifiers", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap index 3c333a9fc43..abd4ae001ec 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap @@ -5,6 +5,7 @@ expression: empty_column_families [ "balance_by_transparent_addr: no entries", "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap index cb8ac5f6aed..8b114ddce4d 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap index cb8ac5f6aed..8b114ddce4d 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap index a2abce2083b..2d119139d26 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap @@ -11,6 +11,7 @@ expression: empty_column_families "height_by_hash: no entries", "history_tree: no entries", "orchard_anchors: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_note_commitment_tree: no entries", "orchard_nullifiers: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap index 3c333a9fc43..abd4ae001ec 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap @@ -5,6 +5,7 @@ expression: empty_column_families [ "balance_by_transparent_addr: no entries", "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap index cb8ac5f6aed..8b114ddce4d 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap index cb8ac5f6aed..8b114ddce4d 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap @@ -4,6 +4,7 @@ expression: empty_column_families --- [ "history_tree: no entries", + "orchard_issued_assets: no entries", "orchard_note_commitment_subtree: no entries", "orchard_nullifiers: no entries", "sapling_note_commitment_subtree: no entries", diff --git a/zebra-state/src/service/finalized_state/zebra_db/block.rs b/zebra-state/src/service/finalized_state/zebra_db/block.rs index 4dc3a801ef3..6f0d2340b91 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block.rs @@ -463,7 +463,7 @@ impl DiskWriteBatch { // which is already present from height 1 to the first shielded transaction. // // In Zebra we include the nullifiers and note commitments in the genesis block because it simplifies our code. - self.prepare_shielded_transaction_batch(db, finalized)?; + self.prepare_shielded_transaction_batch(zebra_db, finalized)?; self.prepare_trees_batch(zebra_db, finalized, prev_note_commitment_trees)?; // # Consensus diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs index 194f2202a87..4f58b505cc0 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs @@ -136,6 +136,8 @@ fn test_block_db_round_trip_with( height: Height(0), new_outputs, transaction_hashes, + // FIXME: Do we need to (and can we) genereate real arbitrary transaction_sighashes? + transaction_sighashes: None, deferred_balance: None, }) }; diff --git a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs index 4bba75b1891..abf85e1aa62 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs @@ -19,13 +19,16 @@ use std::{ use zebra_chain::{ block::Height, - orchard, + orchard::{self}, parallel::tree::NoteCommitmentTrees, sapling, sprout, subtree::{NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, transaction::Transaction, }; +#[cfg(feature = "tx_v6")] +use zebra_chain::orchard_zsa::{AssetBase, AssetState, IssuedAssetChanges}; + use crate::{ request::{FinalizedBlock, Treestate}, service::finalized_state::{ @@ -36,11 +39,34 @@ use crate::{ BoxError, }; +#[cfg(feature = "tx_v6")] +use crate::service::finalized_state::TypedColumnFamily; + // Doc-only items #[allow(unused_imports)] use zebra_chain::subtree::NoteCommitmentSubtree; +#[cfg(feature = "tx_v6")] +/// The name of the chain value pools column family. +/// +/// This constant should be used so the compiler can detect typos. +pub const ISSUED_ASSETS: &str = "orchard_issued_assets"; + +#[cfg(feature = "tx_v6")] +/// The type for reading value pools from the database. +/// +/// This constant should be used so the compiler can detect incorrectly typed accesses to the +/// column family. +pub type IssuedAssetsCf<'cf> = TypedColumnFamily<'cf, AssetBase, AssetState>; + impl ZebraDb { + #[cfg(feature = "tx_v6")] + /// Returns a typed handle to the `history_tree` column family. + pub(crate) fn issued_assets_cf(&self) -> IssuedAssetsCf { + IssuedAssetsCf::new(&self.db, ISSUED_ASSETS) + .expect("column family was created when database was created") + } + // Read shielded methods /// Returns `true` if the finalized state contains `sprout_nullifier`. @@ -410,6 +436,12 @@ impl ZebraDb { Some(subtree_data.with_index(index)) } + #[cfg(feature = "tx_v6")] + /// Get the orchard issued asset state for the finalized tip. + pub fn issued_asset(&self, asset_base: &AssetBase) -> Option { + self.issued_assets_cf().zs_get(asset_base) + } + /// Returns the shielded note commitment trees of the finalized tip /// or the empty trees if the state is empty. /// Additionally, returns the sapling and orchard subtrees for the finalized tip if @@ -437,16 +469,19 @@ impl DiskWriteBatch { /// - Propagates any errors from updating note commitment trees pub fn prepare_shielded_transaction_batch( &mut self, - db: &DiskDb, + zebra_db: &ZebraDb, finalized: &FinalizedBlock, ) -> Result<(), BoxError> { let FinalizedBlock { block, .. } = finalized; // Index each transaction's shielded data for transaction in &block.transactions { - self.prepare_nullifier_batch(db, transaction)?; + self.prepare_nullifier_batch(&zebra_db.db, transaction)?; } + #[cfg(feature = "tx_v6")] + self.prepare_issued_assets_batch(zebra_db, finalized)?; + Ok(()) } @@ -480,6 +515,41 @@ impl DiskWriteBatch { Ok(()) } + #[cfg(feature = "tx_v6")] + /// Prepare a database batch containing `finalized.block`'s asset issuance + /// and return it (without actually writing anything). + /// + /// # Errors + /// + /// - Returns an error if asset state changes cannot be calculated from the block's transactions + #[allow(clippy::unwrap_in_result)] + pub fn prepare_issued_assets_batch( + &mut self, + zebra_db: &ZebraDb, + finalized: &FinalizedBlock, + ) -> Result<(), BoxError> { + let mut batch = zebra_db.issued_assets_cf().with_batch_for_writing(self); + let asset_changes = if let Some(asset_changes) = finalized.issued_asset_changes.as_ref() { + asset_changes.clone() + } else { + // Recalculate changes from transactions if not provided. + // This happens for Checkpoint Verified Blocks loaded during startup. + // We use trusted validation (no signature verification) since these blocks + // are within checkpoint ranges and already considered valid. + IssuedAssetChanges::validate_and_get_changes( + &finalized.block.transactions, + None, // No sighashes - uses trusted validation without signature checks + |asset_base| zebra_db.issued_asset(asset_base), + ) + .map_err(|_| BoxError::from("invalid issued assets changes"))? + }; + // Write only the new states to the database + for (asset_base, (_old_state, new_state)) in asset_changes.iter() { + batch = batch.zs_insert(asset_base, new_state); + } + Ok(()) + } + /// Prepare a database batch containing the note commitment and history tree updates /// from `finalized.block`, and return it (without actually writing anything). /// diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index 08d64455024..60d2b6e44ad 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -10,6 +10,7 @@ use std::{ use zebra_chain::{ block::{self, Block}, + orchard_zsa::{AssetBase, IssuedAssetChanges}, parameters::Network, sprout, transparent, }; @@ -17,7 +18,7 @@ use zebra_chain::{ use crate::{ constants::MAX_NON_FINALIZED_CHAIN_FORKS, request::{ContextuallyVerifiedBlock, FinalizableBlock}, - service::{check, finalized_state::ZebraDb}, + service::{check, finalized_state::ZebraDb, read}, SemanticallyVerifiedBlock, ValidateContextError, }; @@ -325,6 +326,15 @@ impl NonFinalizedState { finalized_state, )?; + #[cfg(feature = "tx_v6")] + let issued_assets = IssuedAssetChanges::validate_and_get_changes( + &prepared.block.transactions, + prepared.transaction_sighashes.as_deref(), + |asset_base: &AssetBase| { + read::asset_state(Some(&new_chain), finalized_state, asset_base) + }, + )?; + // Reads from disk check::anchors::block_sapling_orchard_anchors_refer_to_final_treestates( finalized_state, @@ -343,6 +353,9 @@ impl NonFinalizedState { let contextual = ContextuallyVerifiedBlock::with_block_and_spent_utxos( prepared.clone(), spent_utxos.clone(), + // TODO: Refactor this into repeated `With::with()` calls, see http_request_compatibility module. + #[cfg(feature = "tx_v6")] + issued_assets, ) .map_err(|value_balance_error| { ValidateContextError::CalculateBlockChainValueChange { diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index a002c301766..91adf48f927 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -21,13 +21,18 @@ use zebra_chain::{ primitives::Groth16Proof, sapling, sprout, subtree::{NoteCommitmentSubtree, NoteCommitmentSubtreeData, NoteCommitmentSubtreeIndex}, - transaction::Transaction::*, - transaction::{self, Transaction}, + transaction::{ + self, + Transaction::{self, *}, + }, transparent, value_balance::ValueBalance, work::difficulty::PartialCumulativeWork, }; +#[cfg(feature = "tx_v6")] +use zebra_chain::orchard_zsa::{AssetBase, AssetState, IssuedAssetChanges}; + use crate::{ request::Treestate, service::check, ContextuallyVerifiedBlock, HashOrHeight, OutputLocation, TransactionLocation, ValidateContextError, @@ -174,6 +179,12 @@ pub struct ChainInner { pub(crate) orchard_subtrees: BTreeMap>, + #[cfg(feature = "tx_v6")] + /// A partial map of `issued_assets` with entries for asset states that were updated in + /// this chain. + // TODO: Add reference to ZIP + pub(crate) issued_assets: HashMap, + // Nullifiers // /// The Sprout nullifiers revealed by `blocks`. @@ -237,6 +248,8 @@ impl Chain { orchard_anchors_by_height: Default::default(), orchard_trees_by_height: Default::default(), orchard_subtrees: Default::default(), + #[cfg(feature = "tx_v6")] + issued_assets: Default::default(), sprout_nullifiers: Default::default(), sapling_nullifiers: Default::default(), orchard_nullifiers: Default::default(), @@ -937,6 +950,47 @@ impl Chain { } } + #[cfg(feature = "tx_v6")] + /// Returns the Orchard issued asset state if one is present in + /// the chain for the provided asset base. + pub fn issued_asset(&self, asset_base: &AssetBase) -> Option { + self.issued_assets.get(asset_base).cloned() + } + + #[cfg(feature = "tx_v6")] + /// Remove the History tree index at `height`. + fn revert_issued_assets( + &mut self, + position: RevertPosition, + issued_asset_changes: &IssuedAssetChanges, + ) { + if position == RevertPosition::Root { + // TODO: Consider evicting issued-asset entries from the non-finalized in-memory cache. + // We may add `updated_at_height` to track last update and drop “old” entries occasionally. + // Doing that here on root reverts might be too aggressive (happens ~every 100 blocks ≈ 2 hours). + // Eviction would only move reads to disk (DB); issuance/burn verification must still work, + // just with slightly worse performance due to extra DB reads. + } else { + trace!( + ?position, + "restoring previous issued asset states for tip block" + ); + // Simply restore the old states + for (asset_base, (old_state, new_state)) in issued_asset_changes.iter() { + assert_eq!( + self.issued_assets.get(asset_base), + Some(new_state), + "tip revert: current state differs from recorded new_state for {:?}", + asset_base + ); + match old_state { + Some(state) => self.issued_assets.insert(*asset_base, *state), + None => self.issued_assets.remove(asset_base), + }; + } + } + } + /// Adds the Orchard `tree` to the tree and anchor indexes at `height`. /// /// `height` can be either: @@ -1439,6 +1493,36 @@ impl Chain { self.add_history_tree(height, history_tree); + #[cfg(feature = "tx_v6")] + for (asset_base, (old_state_from_block, new_state)) in + contextually_valid.issued_asset_changes.iter() + { + self.issued_assets + .entry(*asset_base) + .and_modify(|current_state| { + assert_eq!( + old_state_from_block.as_ref(), + Some(&*current_state), + "issued asset state mismatch for {:?}", + asset_base + ); + *current_state = *new_state; + }) + .or_insert_with(|| { + // FIXME: This assert fails if the asset was removed in revert_issued_assets + // at RevertPosition::Root position. And it looks like it's a legal case + // when the previous state in block is not None but the state item was + // removed during eviction in revert_issued_assets. So we should not use + // this check here? + //assert!( + // old_state_from_block.is_none(), + // "issued asset state mismatch for {:?}", + // asset_base + //); + *new_state + }); + } + Ok(()) } @@ -1677,6 +1761,9 @@ impl UpdateWith for Chain { &contextually_valid.chain_value_pool_change, ); + #[cfg(feature = "tx_v6")] + let issued_asset_changes = &contextually_valid.issued_asset_changes; + // remove the blocks hash from `height_by_hash` assert!( self.height_by_hash.remove(&hash).is_some(), @@ -1696,21 +1783,22 @@ impl UpdateWith for Chain { for (transaction, transaction_hash) in block.transactions.iter().zip(transaction_hashes.iter()) { - let ( - inputs, - outputs, - joinsplit_data, - sapling_shielded_data_per_spend_anchor, - sapling_shielded_data_shared_anchor, - orchard_shielded_data, - ) = match transaction.deref() { + let transaction_data = match transaction.deref() { V4 { inputs, outputs, joinsplit_data, sapling_shielded_data, .. - } => (inputs, outputs, joinsplit_data, sapling_shielded_data, &None, &None), + } => ( + inputs, + outputs, + joinsplit_data, + sapling_shielded_data, + &None, + &None, + #[cfg(feature = "tx_v6")] + &None), V5 { inputs, outputs, @@ -1724,13 +1812,15 @@ impl UpdateWith for Chain { &None, sapling_shielded_data, orchard_shielded_data, + #[cfg(feature = "tx_v6")] + &None, ), #[cfg(feature = "tx_v6")] V6 { inputs, outputs, sapling_shielded_data, - orchard_shielded_data: _, + orchard_shielded_data, .. } => ( inputs, @@ -1738,14 +1828,35 @@ impl UpdateWith for Chain { &None, &None, sapling_shielded_data, - // FIXME: support V6 shielded data? - &None, //orchard_shielded_data, + &None, + orchard_shielded_data, ), V1 { .. } | V2 { .. } | V3 { .. } => unreachable!( "older transaction versions only exist in finalized blocks, because of the mandatory canopy checkpoint", ), }; + #[cfg(not(feature = "tx_v6"))] + let ( + inputs, + outputs, + joinsplit_data, + sapling_shielded_data_per_spend_anchor, + sapling_shielded_data_shared_anchor, + orchard_shielded_data_vanilla, + ) = transaction_data; + + #[cfg(feature = "tx_v6")] + let ( + inputs, + outputs, + joinsplit_data, + sapling_shielded_data_per_spend_anchor, + sapling_shielded_data_shared_anchor, + orchard_shielded_data_vanilla, + orchard_shielded_data_zsa, + ) = transaction_data; + // remove the utxos this produced self.revert_chain_with(&(outputs, transaction_hash, new_outputs), position); // reset the utxos this consumed @@ -1762,7 +1873,9 @@ impl UpdateWith for Chain { self.revert_chain_with(joinsplit_data, position); self.revert_chain_with(sapling_shielded_data_per_spend_anchor, position); self.revert_chain_with(sapling_shielded_data_shared_anchor, position); - self.revert_chain_with(orchard_shielded_data, position); + self.revert_chain_with(orchard_shielded_data_vanilla, position); + #[cfg(feature = "tx_v6")] + self.revert_chain_with(orchard_shielded_data_zsa, position); } // TODO: move these to the shielded UpdateWith.revert...()? @@ -1773,6 +1886,10 @@ impl UpdateWith for Chain { // TODO: move this to the history tree UpdateWith.revert...()? self.remove_history_tree(position, height); + #[cfg(feature = "tx_v6")] + // In revert_chain_with for ContextuallyVerifiedBlock: + self.revert_issued_assets(position, issued_asset_changes); + // revert the chain value pool balances, if needed self.revert_chain_with(chain_value_pool_change, position); } diff --git a/zebra-state/src/service/non_finalized_state/tests/prop.rs b/zebra-state/src/service/non_finalized_state/tests/prop.rs index 2a1adf65c20..f28689c9722 100644 --- a/zebra-state/src/service/non_finalized_state/tests/prop.rs +++ b/zebra-state/src/service/non_finalized_state/tests/prop.rs @@ -52,6 +52,8 @@ fn push_genesis_chain() -> Result<()> { ContextuallyVerifiedBlock::with_block_and_spent_utxos( block, only_chain.unspent_utxos(), + #[cfg(feature = "tx_v6")] + Default::default(), ) .map_err(|e| (e, chain_values.clone())) .expect("invalid block value pool change"); @@ -148,6 +150,8 @@ fn forked_equals_pushed_genesis() -> Result<()> { let block = ContextuallyVerifiedBlock::with_block_and_spent_utxos( block, partial_chain.unspent_utxos(), + #[cfg(feature = "tx_v6")] + Default::default() )?; partial_chain = partial_chain .push(block) @@ -166,8 +170,12 @@ fn forked_equals_pushed_genesis() -> Result<()> { ); for block in chain.iter().cloned() { - let block = - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, full_chain.unspent_utxos())?; + let block = ContextuallyVerifiedBlock::with_block_and_spent_utxos( + block, + full_chain.unspent_utxos(), + #[cfg(feature = "tx_v6")] + Default::default() + )?; // Check some properties of the genesis block and don't push it to the chain. if block.height == block::Height(0) { @@ -210,7 +218,9 @@ fn forked_equals_pushed_genesis() -> Result<()> { // same original full chain. for block in chain.iter().skip(fork_at_count).cloned() { let block = - ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, forked.unspent_utxos())?; + ContextuallyVerifiedBlock::with_block_and_spent_utxos(block, forked.unspent_utxos(), + #[cfg(feature = "tx_v6")] + Default::default())?; forked = forked.push(block).expect("forked chain push is valid"); } diff --git a/zebra-state/src/service/read.rs b/zebra-state/src/service/read.rs index 0188ca1bf5e..1450a390348 100644 --- a/zebra-state/src/service/read.rs +++ b/zebra-state/src/service/read.rs @@ -38,6 +38,10 @@ pub use find::{ find_chain_headers, hash_by_height, height_by_hash, next_median_time_past, non_finalized_state_contains_block_hash, tip, tip_height, tip_with_value_balance, }; + +#[cfg(feature = "tx_v6")] +pub use find::asset_state; + pub use tree::{orchard_subtrees, orchard_tree, sapling_subtrees, sapling_tree}; #[cfg(any(test, feature = "proptest-impl"))] diff --git a/zebra-state/src/service/read/find.rs b/zebra-state/src/service/read/find.rs index e9d557dbfb2..e87c396199f 100644 --- a/zebra-state/src/service/read/find.rs +++ b/zebra-state/src/service/read/find.rs @@ -25,6 +25,9 @@ use zebra_chain::{ value_balance::ValueBalance, }; +#[cfg(feature = "tx_v6")] +use zebra_chain::orchard_zsa::{AssetBase, AssetState}; + use crate::{ constants, service::{ @@ -679,3 +682,14 @@ pub(crate) fn calculate_median_time_past(relevant_chain: Vec>) -> Dat DateTime32::try_from(median_time_past).expect("valid blocks have in-range times") } + +#[cfg(feature = "tx_v6")] +/// Return the [`AssetState`] for the provided [`AssetBase`], if it exists in the provided chain. +pub fn asset_state(chain: Option, db: &ZebraDb, asset_base: &AssetBase) -> Option +where + C: AsRef, +{ + chain + .and_then(|chain| chain.as_ref().issued_asset(asset_base)) + .or_else(|| db.issued_asset(asset_base)) +} diff --git a/zebra-test/src/vectors/orchard_zsa_workflow_blocks.rs b/zebra-test/src/vectors/orchard_zsa_workflow_blocks.rs index ee597f38e67..aaa47b67792 100644 --- a/zebra-test/src/vectors/orchard_zsa_workflow_blocks.rs +++ b/zebra-test/src/vectors/orchard_zsa_workflow_blocks.rs @@ -1,23 +1,55 @@ -//! OrchardZSA test vectors +//! OrchardZSA workflow test blocks + +#![allow(missing_docs)] use hex::FromHex; use lazy_static::lazy_static; +/// Represents a serialized block and its validity status. +pub struct OrchardWorkflowBlock { + /// Serialized byte data of the block. + pub bytes: Vec, + /// Indicates whether the block is valid. + pub is_valid: bool, +} + +fn decode_bytes(hex: &str) -> Vec { + >::from_hex((hex).trim()).expect("Block bytes are in valid hex representation") +} + lazy_static! { -/// Test blocks for a Zcash Shielded Assets (ZSA) workflow. -/// The sequence demonstrates issuing, transferring and burning a custom -/// asset, then finalising the issuance and attempting an extra issue. -pub static ref ORCHARD_ZSA_WORKFLOW_BLOCKS: [Vec; 5] = [ + /// Test blocks for a Zcash Shielded Assets (ZSA) workflow. + /// The sequence demonstrates issuing, transferring and burning a custom + /// asset, then finalising the issuance and attempting an extra issue. + pub static ref ORCHARD_ZSA_WORKFLOW_BLOCKS: Vec = vec![ // Issue: 1000 - include_str!("orchard-zsa-workflow-block-1.txt").trim(), + OrchardWorkflowBlock { + bytes: decode_bytes(include_str!("orchard-zsa-workflow-block-1.txt")), + is_valid: true + }, + // Transfer - include_str!("orchard-zsa-workflow-block-2.txt").trim(), + OrchardWorkflowBlock { + bytes: decode_bytes(include_str!("orchard-zsa-workflow-block-2.txt")), + is_valid: true + }, + // Burn: 7, Burn: 2 - include_str!("orchard-zsa-workflow-block-3.txt").trim(), + OrchardWorkflowBlock { + bytes: decode_bytes(include_str!("orchard-zsa-workflow-block-3.txt")), + is_valid: true + }, + // Issue: finalize - include_str!("orchard-zsa-workflow-block-4.txt").trim(), + OrchardWorkflowBlock { + bytes: decode_bytes(include_str!("orchard-zsa-workflow-block-4.txt")), + is_valid: true + }, + // Try to issue: 2000 - include_str!("orchard-zsa-workflow-block-5.txt").trim(), - ] - .map(|hex| >::from_hex(hex).expect("Block bytes are in valid hex representation")); + OrchardWorkflowBlock { + bytes: decode_bytes(include_str!("orchard-zsa-workflow-block-5.txt")), + is_valid: false + }, + ]; }