diff --git a/Cargo.lock b/Cargo.lock index 3eb62694b34..b064f49b489 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4629,6 +4629,7 @@ dependencies = [ "displaydoc", "futures 0.3.15", "hex", + "itertools 0.10.1", "lazy_static", "metrics", "once_cell", diff --git a/zebra-consensus/src/script.rs b/zebra-consensus/src/script.rs index f57577ee38a..a000cbc2417 100644 --- a/zebra-consensus/src/script.rs +++ b/zebra-consensus/src/script.rs @@ -5,7 +5,7 @@ use tracing::Instrument; use zebra_chain::{parameters::NetworkUpgrade, transparent}; use zebra_script::CachedFfiTransaction; -use zebra_state::Utxo; +use zebra_state::OrderedUtxo; use crate::BoxError; @@ -59,7 +59,7 @@ pub struct Request { /// A set of additional UTXOs known in the context of this verification request. /// /// This allows specifying additional UTXOs that are not already known to the chain state. - pub known_utxos: Arc>, + pub known_utxos: Arc>, /// The network upgrade active in the context of this verification request. /// /// Because the consensus branch ID changes with each network upgrade, @@ -111,7 +111,7 @@ where tracing::trace!("awaiting outpoint lookup"); let utxo = if let Some(output) = known_utxos.get(&outpoint) { tracing::trace!("UXTO in known_utxos, discarding query"); - output.clone() + output.utxo.clone() } else if let zebra_state::Response::Utxo(utxo) = query.await? { utxo } else { diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index d7b164e0fc0..48a6d573df2 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -70,7 +70,7 @@ pub enum Request { /// The transaction itself. transaction: Arc, /// Additional UTXOs which are known at the time of verification. - known_utxos: Arc>, + known_utxos: Arc>, /// The height of the block containing this transaction. height: block::Height, }, @@ -100,7 +100,7 @@ impl Request { } /// The set of additional known unspent transaction outputs that's in this request. - pub fn known_utxos(&self) -> Arc> { + pub fn known_utxos(&self) -> Arc> { match self { Request::Block { known_utxos, .. } => known_utxos.clone(), Request::Mempool { .. } => HashMap::new().into(), diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 8fb52ccbc17..555e92a8544 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -17,7 +17,7 @@ use zebra_chain::{ }, transparent::{self, CoinbaseData}, }; -use zebra_state::Utxo; +use zebra_state::OrderedUtxo; use super::{check, Request, Verifier}; @@ -823,23 +823,27 @@ fn v5_with_sapling_spends() { /// First, this creates a fake unspent transaction output from a fake transaction included in the /// specified `previous_utxo_height` block height. This fake [`Utxo`] also contains a simple script /// that can either accept or reject any spend attempt, depending on if `script_should_succeed` is -/// `true` or `false`. +/// `true` or `false`. Since the `tx_index_in_block` is irrelevant for blocks that have already +/// been verified, it is set to `1`. /// /// Then, a [`transparent::Input::PrevOut`] is created that attempts to spend the previously created fake -/// UTXO. A new UTXO is created with the [`transparent::Output`] resulting from the spend. +/// UTXO to a new [`transparent::Output`]. /// /// Finally, the initial fake UTXO is placed in a `known_utxos` [`HashMap`] so that it can be /// retrieved during verification. /// /// The function then returns the generated transparent input and output, as well as the /// `known_utxos` map. +/// +/// Note: `known_utxos` is only intended to be used for UTXOs within the same block, +/// so future verification changes might break this mocking function. fn mock_transparent_transfer( previous_utxo_height: block::Height, script_should_succeed: bool, ) -> ( transparent::Input, transparent::Output, - HashMap, + HashMap, ) { // A script with a single opcode that accepts the transaction (pushes true on the stack) let accepting_script = transparent::Script::new(&[1, 1]); @@ -863,11 +867,7 @@ fn mock_transparent_transfer( lock_script, }; - let previous_utxo = Utxo { - output: previous_output, - height: previous_utxo_height, - from_coinbase: false, - }; + let previous_utxo = OrderedUtxo::new(previous_output, previous_utxo_height, false, 1); // Use the `previous_outpoint` as input let input = transparent::Input::PrevOut { diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml index 4250e060786..55000551771 100644 --- a/zebra-state/Cargo.toml +++ b/zebra-state/Cargo.toml @@ -10,23 +10,25 @@ edition = "2018" [dependencies] zebra-chain = { path = "../zebra-chain" } -dirs = "3.0.2" +chrono = "0.4.19" hex = "0.4.3" +itertools = "0.10.1" lazy_static = "1.4.0" regex = "1" serde = { version = "1", features = ["serde_derive"] } +displaydoc = "0.2.2" futures = "0.3.15" metrics = "0.13.0-alpha.8" tower = { version = "0.4", features = ["buffer", "util"] } tracing = "0.1" thiserror = "1.0.25" tokio = { version = "0.3.6", features = ["sync"] } -displaydoc = "0.2.2" + +dirs = "3.0.2" +rlimit = "0.5.4" rocksdb = "0.16.0" tempdir = "0.3.7" -chrono = "0.4.19" -rlimit = "0.5.4" [dev-dependencies] zebra-chain = { path = "../zebra-chain", features = ["proptest-impl"] } diff --git a/zebra-state/proptest-regressions/service/finalized_state/tests/prop.txt b/zebra-state/proptest-regressions/service/finalized_state/tests/prop.txt new file mode 100644 index 00000000000..a0cb388e6f8 --- /dev/null +++ b/zebra-state/proptest-regressions/service/finalized_state/tests/prop.txt @@ -0,0 +1,9 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 05c40f7989ccbff7872bd31bb8c6d0509ba076d72bbe210f10b6dff81c141fa0 # shrinks to mut join_split = JoinSplit { vpub_old: Amount(0), vpub_new: Amount(0), anchor: Root("0000000000000000000000000000000000000000000000000000000000000000"), nullifiers: [Nullifier([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), Nullifier([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])], commitments: [NoteCommitment([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), NoteCommitment([0, 0, 0, 0, 0, 3, 175, 76, 65, 80, 142, 175, 73, 247, 62, 189, 255, 79, 76, 197, 15, 80, 158, 176, 201, 34, 19, 12, 171, 146, 86, 8])], ephemeral_key: PublicKey(MontgomeryPoint([226, 36, 109, 232, 133, 17, 126, 205, 23, 115, 124, 166, 51, 251, 53, 19, 223, 105, 90, 178, 34, 239, 10, 180, 152, 201, 73, 151, 241, 212, 26, 177])), random_seed: [109, 52, 75, 252, 134, 2, 110, 133, 207, 156, 2, 26, 14, 120, 108, 157, 128, 82, 178, 140, 172, 142, 117, 26, 23, 20, 82, 129, 4, 150, 51, 17], vmacs: [Mac([241, 208, 251, 98, 153, 87, 53, 232, 246, 127, 221, 211, 8, 194, 2, 135, 214, 43, 185, 24, 200, 195, 73, 166, 63, 234, 154, 226, 61, 81, 160, 60]), Mac([34, 253, 200, 50, 151, 184, 200, 56, 128, 179, 113, 232, 146, 173, 147, 6, 166, 1, 202, 221, 246, 99, 212, 6, 81, 194, 79, 142, 38, 236, 129, 188])], zkproof: Groth16Proof("fa751788d36927e080b4f3ee3595094dc8e12a0cba0377128f977b8ecc672385076b73cde64e5ad12d32182ddf4eb1603cb63baab18c33dc7c7e6617c3c004644982f233f9b63005c9f819b32eec1d162009ab08f322b360a245c708ac91f17568ae39e056a2185db98918e36251b7f2fc1e1f6d8e499a06a52d2ee1a5ac374015fa908af2100bcb1bc900876f3e8096f4d0c55bcb26ad71248c8764e7131f9765089789727bfe96aef5f0269e09467d23ffa1f54ad24b019b1416352221ff19"), enc_ciphertexts: [EncryptedNote("5d278fe94aa14a1352c8da227bc8328a99061b6852aee4d37902aa3cd5356eaaed82d8dfb2a39da917814d15abfdcba1d569af90c5a7d5d6e601c314528586fc848d9106cd7746414b787b3f0ffef61aee7e13b3f2dd815c8c0454360386a00edb03c535d86847df24185e4f28db70798fdab71bb373e549e43d94b455c14b2b95e0fb963d9282ae36d7cf395d4832e31b3586ca280555af5a02a9b1ee49b1c49539041133fa20525fe3f90515c502bfb499b5bd654ffe7cc9fd071968c3179a4a9674060bdd7d7413a387f62d2d3dde2656b41f972a07b200b38618ff1067adbb06d05ca72a7689753552f6efe154e044448a17f6e1575431eba32989a46e7f9edcdfdc15ac59571c97f6e6111465e99f0446c2f4bb8d69f261a47337badeff92f3e7078899f40976b2dd30d9c4afbc3a50e7338c340ffefef237011261ae68507ead75944bc3a521896c3b89d15aab6fe67aa9eb8db4aceaf99c1cff58802fab475a17972f2af72a36570e69fc59a7d030c6fb96f9523922caee8ad6f1f6d96a431b33ad4364296af95fd1ccf2089053d2b82814725259ce1497459d11cc05fecbf5f4a6c73b2c1b76193f8ce0ab33848e40e82ccb58368fae034ef6fbcd2c34188afa08e730a8258ce6fbf0961c4a9fef2a0e9b28ddc4879edeeb1735fa0d0bdd73385299550a6e1a4db20198565c915fa2478ac32ab467a2e47afb071a020928412210d1c44418b2aa829dcf13dfd15dd3d207f7a31098acfa86ed9e6dbd2b38a6de1f44da687b0edcda682d941af3a9bb36f3e28ce90820f654503460fcc58198881ea5d12c2e0625a14d8f6ba1a715810f826a390877"), EncryptedNote("87d9483e5f1cb9cdd53675f292cd652b52f2e02cc8601faffdb4ca3844112a455b5bcbcde0c2ecf905b41df36b9577fd069bf94e5afabc3eada585b6618b533af188934505442abbff0e77488d82f31937ae27a98c8c67c51f3c543aae90367c20c46c7176b0317cc8aca4783b0fcb4318c74612cd26f09b30cd196d65e5228e08fabd37af78fb9492414358ea7bb1be54c57780a0788627046f243f17027ab98dcfea00238328fd66d427304f4294d929c1d2f8d4561521f56ddd55782da290500ebb3a6f3768cca513f22de616eb2de9ef78b5308339375f7380fd96ff515e0f5b60a5a3fc8b7c46e18e4b64f576350f1042dd8739f27109ff1fdfa4f118d2b40700e2100d3c9d43e45bb54810b043bf8fb72ca0580ba65e3749aca8a8c672f27738b88c46190e31a93671c9d2cae27cbb1e4d7f2b01b523ecbc2209bb29abbe06e604c14760acb03815abd5f2eced39298b2b2a701e83caa4550c319fa84bdaa2c634bde49f6cef9caa1c0ab17cdb5d4e729701a54e869c0d87d71272301cfe70953e2140c59e6670b04d6592afee8ba440b22ce51bba790e421313fb2b2c2c3108f2e4de4c7ffd348885febe6596526c37fa6f3c1368cb148d344a43773f520d100a07986eec076fafce49c1b46934cec8088abb23fd6b357f59af19c38e07a81725753178f60ac10c62af088ba954760d9ef06887385749b187fe9b69fbcfbc51fc1e1773c30017fd376a1535d28aa670aa0a52897cba639467e381b32f404245fbd48a40f565f6d84c8f577f6a57fa454a426441230c1eee52245107aefb7fabd342ba6cb4d5de82b45dd089d19df051eaa8ae0734df")] }, mut join_split_data = JoinSplitData { first: JoinSplit { vpub_old: Amount(1165035197975115), vpub_new: Amount(1368182949535973), anchor: Root("b8d8bb6fab14552110e0e4f2925698dfc23040ae14174e8f2b6d7fc3d05a42aa"), nullifiers: [Nullifier([195, 162, 210, 153, 33, 89, 117, 5, 84, 64, 251, 91, 60, 235, 97, 163, 251, 199, 220, 208, 254, 253, 200, 137, 84, 160, 18, 7, 195, 99, 172, 102]), Nullifier([86, 164, 56, 205, 124, 229, 97, 74, 243, 139, 254, 152, 98, 123, 159, 217, 57, 60, 15, 235, 56, 23, 216, 5, 225, 40, 249, 17, 15, 102, 119, 83])], commitments: [NoteCommitment([203, 233, 10, 210, 153, 165, 19, 118, 206, 147, 134, 166, 55, 107, 31, 29, 85, 17, 111, 21, 234, 177, 19, 12, 193, 36, 223, 15, 74, 230, 42, 91]), NoteCommitment([203, 240, 89, 138, 182, 29, 93, 82, 199, 154, 137, 188, 38, 138, 240, 110, 22, 21, 75, 132, 102, 43, 126, 143, 217, 228, 128, 95, 151, 150, 128, 94])], ephemeral_key: PublicKey(MontgomeryPoint([193, 156, 104, 164, 135, 8, 194, 79, 23, 17, 41, 53, 236, 223, 186, 98, 189, 95, 0, 115, 51, 87, 105, 198, 167, 176, 171, 110, 44, 187, 185, 254])), random_seed: [237, 61, 93, 223, 242, 152, 53, 243, 124, 94, 149, 177, 41, 85, 250, 205, 156, 50, 105, 205, 169, 52, 219, 190, 16, 169, 174, 213, 230, 169, 82, 93], vmacs: [Mac([107, 35, 100, 99, 194, 230, 41, 33, 149, 155, 185, 19, 210, 196, 110, 116, 105, 132, 101, 152, 3, 40, 104, 215, 96, 32, 226, 217, 139, 199, 28, 124]), Mac([9, 129, 248, 174, 26, 177, 176, 80, 127, 20, 254, 9, 50, 150, 181, 92, 23, 218, 166, 203, 159, 195, 251, 36, 197, 151, 116, 40, 12, 201, 20, 32])], zkproof: Groth16Proof("3912f1eb2e5531e261f146efaa7f5cd2452ff7d014342ef06cd79900c2138a1f9c010f3f0e1b0c45d8b67c5ba43198ebb0b4ab2722ad4bfed1e571fd03444d9095402a26aeaf2da9d90443c2e5ea0f2ce72e191c34014b939d9e089e9d585d92cf65a6621e612461c17faec84254ea97d1931e8e59ed60ef8c84180894db366e60cbb0628fc72f0d58dc4abc5ea1d38b047648335759fdaf0d04e5440e3f165d2d6e81ced24cee62f3b01e45e726847483b20b7d3adca3f5d2cafbde68ccbbc7"), enc_ciphertexts: [EncryptedNote("a83d6fb03cdea4b26a21e5df34e7dafb7dbb12d9812eb05de7643e1b05cfa76cbbacd8176ee95132e7975321269a26b178eebb78464cec8858424b3629ffd1c502ff5acff7c12cb83c8306b45eb88a65342b7e263c92e915148326a90536dd1e6ab49774158111410c941b583a33110a2a211492ec7914e6d4098a5252069672b96bf6fac7608931db92cfc666758aca60fe2a9ace2742a8294691116b9739d802195e158689230ad36a4a82706109ff0d06f87d61db8f4521a7ab32f709c032624ce5b8ba491b378042ac92e93ed4172d216abac0b909a2349f55ab3661912ed383e060b1d11ce87898bb7193fc38ddd833faf13445ffaf8d8ec6184906aa3e562406d2d96352174a2e61ef5a388db22f622c2d463f577ea9f00cc89423af7419ab989a7af0227b8373876709f6aad1bfc5b1bafe4703031b04ed4fc0f9ca45acfe8a4689fd2f3c9397c3e819f18a6cb66dc30599a87be48900d48f84602b3cb58f63287839da4ef023542158b97ecf4a4fc2fe829b16f49597c06fd488d6d1dd12bde3d0551125aed63144be428beb06e935d02f0e8c82b8ec6a50ad451eedcdb13eaaaf64baf848fa9052b5c0e2a4ddadfafddd5c072e6ac56f8f5c76549a94485fa178614dfbab314f9d18793461ce4b53c0be4a8b909c24197b9ceb4486e84012a6d8ff526f11c0ac2db6d53b9315538c00fcad3ab5de9c7000c26ad4a92e97c0a60ad58ca0026c4a9bacd3a61f79cb0f41915a32e058964f0c1c01368902594d855941e35ddf0a69a2a1ca1fc068afcea2540698e2611fe612644548622a1b79d42cf7d6ccf43ff6577852750171fb87517f8b0d85bd"), EncryptedNote("556346182f5e2e627288f805bf374b31fba91ae51a063cdaeba7209800b8ef92b4d77e4cc9fd52f6c7ba3d2deaf3d2c5c2a1de2eb9932b1de497aa5462b097a94664860a1021d1d1ee63e8bba28fd2fcf02b5a06a58062f0bff46cacf8a2342604b7bb677f444246e2c2f67e803c1cb16133ae2b72452c839ce9ec87d490e9a7f02b33fe326a7eec72f45ae302e84a01be96d9eaefc066e89decc906f8ffeca7418f382617cf6b53cfa70108bb378e633c72e0842433d82ccb3ebcaa0b60a7f632a1ba1ad598e981c4725373a289109ebad7c6ad3cba5d93932f5ef14c9d6598ca4ed88a6ecb791b1a988a29361d09f76bf76b19c573540dbdef56244d933c1db07a1886536fd3f77232057915f2bb3dabd96b8673ae65502d546696495990624cd67ce24b9a7a330a7672e97fd0e212679dd2f8352b48789b567e9ef6f0a04e27449928a444d78752f3ca0356b17d01b24a9166839bf4ad2803d1844f3ade5e98b292798bcd31641b80bda4e3fe4c85b70554f4b1ce7b5c8afef83fe798f24c3ed49212caf3c81b148db0ef5972dcddc923be2b719f2a6947ced8b3dbc062654be1ee29a10aadd93a4815cf634ba7c31214d8e5b814f874c7bdec0807155338a49108ca6c278ad09d4bb15f1a00a31fea39886b230690ef0385ee78a2d7b41e48ffccab0e5ff66b2e71f02eca42af37d6e1d37b16bda906a657c6d9fcc06ead4000185288a5c0060d337857ead6fa95cff1313a609068a95edae9db7de056fdfae0174ce3c72ce8f51d98ffb3f09e1858cca4d47456e60a642e643ed47c03ac56c5c756346bb69bfe4823e06cd42ed965f2a5db0f9d39024c")] }, rest: [JoinSplit { vpub_old: Amount(956686151574051), vpub_new: Amount(342868006943599), anchor: Root("76b64bac86981790e4f4d0b9461c079d164253afaa421aeb53df2b7515caa480"), nullifiers: [Nullifier([123, 0, 14, 57, 102, 10, 210, 131, 1, 136, 113, 217, 49, 79, 228, 190, 202, 129, 202, 253, 156, 157, 245, 58, 199, 149, 217, 186, 15, 105, 157, 166]), Nullifier([111, 101, 1, 174, 142, 163, 197, 34, 104, 50, 212, 101, 7, 49, 19, 238, 84, 159, 11, 231, 247, 227, 45, 188, 78, 64, 100, 102, 204, 30, 144, 232])], commitments: [NoteCommitment([148, 164, 138, 126, 95, 28, 0, 109, 189, 59, 200, 133, 233, 165, 239, 123, 30, 99, 195, 209, 83, 42, 0, 85, 64, 10, 223, 102, 35, 58, 66, 251]), NoteCommitment([182, 245, 126, 20, 49, 17, 83, 141, 118, 75, 112, 169, 216, 109, 113, 157, 54, 214, 104, 83, 168, 7, 53, 252, 235, 248, 141, 68, 213, 124, 122, 55])], ephemeral_key: PublicKey(MontgomeryPoint([113, 249, 32, 214, 1, 144, 94, 239, 40, 29, 67, 118, 145, 102, 206, 157, 248, 25, 127, 33, 102, 255, 237, 37, 181, 62, 95, 33, 143, 222, 189, 81])), random_seed: [50, 15, 44, 15, 111, 103, 51, 99, 212, 229, 243, 109, 49, 29, 226, 196, 213, 84, 79, 1, 102, 139, 26, 20, 241, 80, 147, 147, 150, 109, 21, 245], vmacs: [Mac([75, 96, 231, 126, 169, 108, 228, 81, 0, 48, 193, 75, 55, 218, 112, 129, 213, 140, 107, 2, 166, 67, 193, 71, 30, 63, 44, 72, 35, 68, 227, 146]), Mac([144, 153, 242, 180, 120, 180, 70, 178, 70, 27, 205, 126, 41, 104, 104, 54, 202, 214, 235, 171, 101, 212, 207, 142, 40, 191, 225, 12, 191, 223, 98, 62])], zkproof: Groth16Proof("246a772e637552bba00d73b829984f291259c25671acd8653e20a85566be2cb5b883e23aa05c0928e75837f1ad8cf878a2e68f64797bef658330f931db9d4ea7d41d40dae7afb16bad78e907f989e96ff990e315bca2b83567f356eec987c0a08782b88e0efa01bf9f5897b0dd13ffa5f17f463a88b4bec3956e61ef9d5c26a36f72796363f0e4decb5cc58590a0f86798e9b531809bce77c9d56b22bc62f97e28a3c6017dc5e4846b0d8a965dbcd89813ee5d371e5014126772ff210d0822ab"), enc_ciphertexts: [EncryptedNote("297ed9f8f04564d8c172ef86af95c301a74bb98cb63c173bac16f4eef724437dd8296be3dfcaa586feab1f69d952d65dd57f466e1ca9602f89bdb42e84ec6e1a500e352286be9c93c33c153e0e292fbb40a857a5d79e1c287badab5d2a25581720b8e2e7f5126fcd5dd4dc9e3cacdae8fe2b230dbc44176fd44f86cd30b9564fe6ad9c15ae851413b634241b8f885e4853c00d5f3587d97984752d9d8340472d187b5a9e7e0878365373f494bcca901c9051857d51026607f548482f737ed2699f91251907be22d0264c96261e0eb54e106426385fd90e31a25923d76c81bf89167b1284c016b52c3532e762381d1dbaad5bedc482de4ba34bcd87c635385e7c52daafb6514de5679376060ee08074a5972cc0365fa7ed34ec3fa25030d7759f9ebe11bf106f101aba7a195838d490672059e20a5800f5b49ffa2f1728365639fc98046a5f1e4d4b51af51e8d1c806e6ccb5f6cbf2de34f6cbd59d00ff6c97225bafbc05ed2880a442f298634c8552d36bf83858cbc3ce7627d2be03f90106821e7ee81bfc387bd6fa617ca1b92bd281548fc5066ed9771b2f1e72918eaf7424f582e57469d25814338d30f4feb16a933327306ff81a56a95e6075b3cc6d0d9f9923af7265e1b53ac0061b0a31bc98e006850b750a759d4d0901a679654f2fc0b6fa27b36fdb55917db988fc9df49893b5844db8f1940a0871e8a744700db1e7a0380eb531e7187a8f31ab297e3e5bdcba14c829998bffbf335e44f1e7ce90195b04e5cf788b491e6a0166b09d2690c765441a66abdc6420082fb7a41f7909f664f955781161419891e9a5341ed4006db6f6e62774bc0e3937"), EncryptedNote("190ba7ede2e9e538e90312b48d45e6381183d870d8e02964c51edd958afca7f51a5bdbfe52ce40a942ffa779268c754d4f2bb850789b0b81baf856b4bf29e3a02a671fdcb887e8b5574489ef5be3f59ad788fde37891696bbb9a3d566bb4055979da23a95a92b7d69e141491549ff384c5ad3da5f6d5bb7f6e276e1c3e36d564b37783b2999667ab3961e2d3a3674720c1fdb8a5a94caf3fc7b9d6f4b74de9175cf5b8a3526f0973f6cdf43e691eb5e05e156fb52ce757b2f8c1e12017869c85b0b629d8d780bee48796a588082d947a63341e32a59944dcb1e0c67dc68f26c39cdee680e89ee17fd8d30a78585aec6928662e401dcaacf4345d2c456315d84a0054cd75b35e5a6337739dca9a3b859fb4f71c9c20f8d100f15b4f5b52e83c2d3d7fb6d0e3c70a23a5ed8948e959f092b364fd00b08155a5519b8e06ee3e47e2f720fccc21226b04b4746b1ce41af4b1ce61c84388d85a2c3626253dfa13b64856ba90526a0163fbf5e325c34a27640b69a93264e4a2fec2b2049dbfd5e6e5d2028f6b76068ff733d2657b8d7b8c0c45ecf058779ffad30d921c4feccc3ae6534324bfae92d1b89069f595d6fc13174ede495e8e3b1c80415593ccbeb09e270e906eb50d38bdafc226cf27e3483a93924c82bbc94ddde7e554036cb9f35e516886e10c098f920d8e0cd9813ae42ad64def1938767da449fef1ccdae3efb478d229fbb2e08afda7a791a04a217c73e658de2c572c0e3baf8af3869768a6a6fefc1f4ebc5b72eb076a71d9d47c7eaed726dceecc15d7236901c4a30aff1e16e7610e6c3f0c41862c2e6ad98727e07638db15a196ad1bb102b0b7")] }], pub_key: VerificationKeyBytes("ebe9414a8e8ac5e427e4bdbe7ebcfddd23db31240c51538c85bc6ae987a8b865"), sig: Signature { R_bytes: "a0c480ae826695bd3de666b67ad5c125453e75b44a92e26e5c86ac033752a6c6", s_bytes: "a72da56b3e74e584790dcefc7084d9622f893e62ad7bafdf1231d3df41e0d895" } } +cc 7f5cdc711a08d381b646d609338e596b4c98e0cb26cb2192ffb0285ec8da2ab1 # shrinks to mut join_split = JoinSplit { vpub_old: Amount(0), vpub_new: Amount(0), anchor: Root("0000000000000000000000000000000000000000000000000000000000000000"), nullifiers: [Nullifier([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), Nullifier([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])], commitments: [NoteCommitment([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), NoteCommitment([62, 170, 56, 115, 186, 145, 210, 31, 143, 255, 68, 79, 74, 54, 241, 31, 33, 140, 184, 214, 252, 8, 178, 103, 24, 109, 31, 239, 84, 165, 182, 34])], ephemeral_key: PublicKey(MontgomeryPoint([168, 204, 247, 86, 68, 239, 216, 57, 218, 115, 133, 185, 87, 113, 26, 49, 20, 69, 246, 178, 183, 187, 150, 82, 4, 108, 221, 65, 38, 251, 32, 236])), random_seed: [174, 251, 44, 40, 8, 97, 96, 68, 208, 181, 69, 213, 98, 253, 178, 176, 173, 86, 53, 194, 101, 145, 254, 254, 31, 140, 217, 57, 239, 54, 10, 178], vmacs: [Mac([141, 240, 107, 153, 69, 139, 135, 129, 24, 67, 59, 7, 173, 242, 79, 248, 126, 83, 71, 185, 198, 223, 220, 9, 25, 209, 242, 28, 166, 213, 159, 66]), Mac([102, 168, 18, 87, 16, 199, 108, 88, 230, 178, 230, 17, 54, 100, 162, 239, 45, 1, 82, 124, 106, 232, 139, 210, 232, 63, 159, 233, 24, 93, 171, 136])], zkproof: Groth16Proof("8c02ad98c4e717e39cee97c959ab8b13d73ffdba544c03878a1337bba6c2ee4d41295bcf8499063a75e01da76f2ccc7a1614efa0480d38f27b45eb98a90fc8c163640119d45c22295b57ab8efd8d65d9bc95a6335dfca99400aa5527e91c9f45a4c54146731c0d886deed7d4f4762608ab37f08d463a95d1afdaa237c83fbb5d3d7f5c131650bd4875524f769dc54d48e0dbe1fe189edf75a1a628bf975c8295d547d6b58a670b1327db92c6e3e0d15b9e7894c1b5b9c898ec4866d973b94299"), enc_ciphertexts: [EncryptedNote("97f77704753ddecd16068b8665e5d31189c9e26e9ee6b4d7c67cba82d3b622a8ed26ed412ecd4e4f940113af1d34dd0da80b93cd0074cf57c43cc00e12f03154a4ebcea687e927569c174719eb284ee78e0428cb2f0fbaaef38c697a6ea99e7d8d3851cb4743bc46099f257af4a8107a25f5fec7e55df73512cf32210db8730a40cdf838db31c083f3c42eb70927cdb763bc6a47f64262a10970785979f9f96acb15e242753d639ef8134b6cca95434b2e729ed33958a653d0cec3ba4d81a38abe9f069de538672eab3bb0618ccb7fb1de1ee20838f40e124e01ad6431fd893c69959486bc72dcf109af54c4bb98256503327aa98796684efb90d4bae26870363fb29291ead2156bb0fc67b56577ccea9c86f6ed4eeae1504305ac8a168e5301431ec47282317ffb85b9db842cd7615a078c7a3980159218e26b625e9f5e0a9a30f542e3588e0e71a49daf4ca7c6a70c96aa3d2b1d24518471f08e3d638e477f2396f8bca336fc988281676131aadacd9d29963e21f673af846ff462c05878b07d822dbbc803beed2c0173eecc99b97d21d6bf860b8304054920702e99b7872e2ec1425c951bd1c6aea16adaf4e8a7cf72755cd478d7c431b2779453994b3334851eee448d21376e64e4d95596dc333c6f5e752d990aad889b06ede2f648de195d1f6a06ad0f46a9ee04733ba29863860913b79645f65bc12e99e488e96aab13e39e8bf4c3bf54ef27d063924e014a1321d537dd4f99e0fa5be9e8c0a13aff10069270c040ebf058a02e705059d3b2a3a80f7ce70f8b369836a73bddd63fcfd363fed0aa43cbe6a107bf1a5233fd5b34c2cbfc637a1605dac3"), EncryptedNote("8b45ca6126894e9051f32506082cf0bb6a364a4cbf54295693f88f8373bfc5cd9b417f83c838714d4a5fcb91fb0f6981435cf396e83cbaefc87b2c10f739a2d3c80206d01f4bec49a3623a622b45e7f12334f464cc6164f8b466f9fce680b8b90f9eb6ca77069697979610a9a538f8ef92865c77ce721fcc3ccdf5bc3c2e7f28e0c666b33e34067f9482e4d7f9d19d2e58bcf15d20a2df1f1450331dc00cabef1fc1bbf926f8bbba4f8d71f422a4ed38d05c8adabc45ccd8fd6fde0ffe07a29484f22457cba2a84907b99b743f529495e7296bf43817e2224b895ccebc8e57cc4b9e59f271ba3d3fa86ea5e59c9cab012c65bf01b68040b406ff1285e88e77fedf844cac4d64227cd8af00b561973cadb8bf5d965fbc5f84a6cd7c2d7e0e8b31e09b48451b5ce24c3298a916aee29f423acceca22fb8fcd90507a32a350bceb2a63a25017c5781c4e5d7480daf105104cc9f9206c0308c7b419d272cb730ca90bb08f06d9179c14d1b148a7075bbe1348138720b195541b6b189a77642b7070e934497031a65f8590c609079e5587b3776614820726264b0a151c036e7e3ee899feed202bb81a56288ce0f0a0fbdf00ec5765c3eb8e6674d6c585b8faf33f55d5b170c0e00604401d68d9c02d660de1c1358e78890fb1631fb96a82302f0fe228f748501ceab2069b145e6ac025028468b42e1dd5833df72eab7238e4e1059049e91a98f1c6a3ef16623f6e130fd508d4f81612d363c39c54d66b836b7707f2b9cab57c1672aea42600634800773f61f5d35fc4efe4de27b62b42d3f5bf0f772f58ee9fb6ed0fad6cf4d64e0cd752d2c7b03849d2d95134d89")] }, mut join_split_data = JoinSplitData { first: JoinSplit { vpub_old: Amount(89538963809245), vpub_new: Amount(745622612227389), anchor: Root("69a1bddd6a2374350cc10fa078206d7c91d71aa9fc5d6cde6f10573983d05a71"), nullifiers: [Nullifier([202, 171, 189, 38, 80, 183, 54, 227, 99, 13, 83, 110, 235, 167, 27, 199, 70, 91, 68, 60, 211, 89, 18, 225, 175, 146, 58, 32, 228, 184, 172, 115]), Nullifier([136, 179, 114, 127, 197, 106, 59, 131, 34, 46, 159, 188, 24, 108, 186, 61, 136, 87, 104, 184, 216, 133, 111, 27, 250, 233, 197, 3, 55, 8, 55, 155])], commitments: [NoteCommitment([217, 142, 223, 146, 125, 193, 13, 217, 62, 221, 203, 1, 82, 180, 49, 229, 78, 33, 168, 30, 77, 130, 60, 217, 179, 153, 113, 21, 240, 217, 120, 246]), NoteCommitment([122, 36, 45, 134, 165, 229, 64, 11, 16, 177, 233, 24, 81, 159, 160, 75, 251, 127, 24, 101, 69, 114, 161, 86, 126, 181, 78, 165, 18, 17, 215, 208])], ephemeral_key: PublicKey(MontgomeryPoint([169, 54, 15, 22, 189, 37, 136, 206, 115, 168, 164, 13, 53, 247, 130, 9, 194, 50, 170, 94, 227, 128, 76, 16, 156, 126, 250, 213, 143, 132, 151, 100])), random_seed: [82, 183, 138, 73, 3, 155, 44, 245, 15, 163, 169, 32, 58, 62, 7, 147, 46, 61, 126, 26, 70, 240, 162, 95, 133, 136, 53, 118, 84, 225, 220, 211], vmacs: [Mac([163, 105, 0, 231, 79, 192, 50, 146, 14, 228, 6, 78, 97, 252, 94, 93, 8, 114, 73, 230, 205, 251, 231, 204, 124, 158, 150, 104, 182, 250, 108, 144]), Mac([43, 53, 0, 179, 19, 178, 38, 20, 47, 65, 139, 152, 95, 117, 205, 179, 48, 195, 86, 73, 219, 211, 106, 45, 250, 171, 138, 83, 237, 164, 99, 228])], zkproof: Groth16Proof("0670b613960dfcf8f77da931ff514e60b002589af8f2f29aba0b877fef6d0d1e3762371b41fa9249bfbee5cc8d64f1a550abc804a1240a7d207bda93022dbb06b8fcfa3d7d64c9b21bc5f17b9d4327e5e5b3638bcd0c876b004fba5aeefb636864d6cf38f288aaff91aff806ceff4a7a9eb8ce32d0006e28412095db4ac9d0dff23c5e60085d4df9116ff46e61aa453f54cf0718617a94def646a20e5097aa1c6447c720e7a5f39097ca2d5df1f181dec4e4ca71d0c459120150666cd9e703e3"), enc_ciphertexts: [EncryptedNote("b69c4ede8906b4ad4f2acfcf13befd45ce582ba1a1ad03e462ce608f5a2804f33f4f95934aedb29acf427176d062cb2a59fd8c733c81c8ab9b6018bc6b325fa0ae40a6bf448b56fa06607821327977903daa91fc860f6db487845c352ba88ada59261b7a768217e1a49507e730a77df593a2ec2f19904ad899556363be66e2276780108eefff467b67cc15fa02a401a115bb3f4484e0ce4c4eded1e4a38ac5d4ee54458620c4e66517a18e62c72bafe4f0197eaa41f489663e7413c05c40cf998fb55b152aa955f2e3d5a2cc422692d1cc430ba988ec88cc909c1ca910b03a3e66312ae8a6822758baead6eb28aa87375e5b125284c0d969dc4b24d85b99ee616f2112aecb64bc10603f2da7bf0105565f5369db73d3f7231b9cde18efb7e86212e1c7d7fc0f3c3ccbdbff1d7fa72c3d82783d26d06293c89478c94ce2aa9014cf4b9cdeb1e9cc0e3ee88a8d8f131d6fa3f26fb38e9906920583034b5be988963ca231ec4da47c59303be56c27f3059518966b3502e7d5db7e3368652cd6efdd042ba402f2910679df295c2e93bc4fac3960b5876a0a9fb561ea87ac6f01df1cffa44bdc939fcc4085514a76c163226fb1d9ad710230710237641062d5c2d5c21ec93a36777dc8433ffbad9f30b57451a26a4b45718cd3aae1c397fafb393ab499af7e924c44315bd9ed7d625edf3feb14d3ce3e19e6008beab20cacdd14cba44aec7e442b4c5515bc487e520d2adb5a9284f95a434995f064961a27ff89145eba4333933f88d7db6161acebbf748f610eede2c0678e59ea81c3943811356b3bd01df80db6cae5054f28a9c448645da164e9c10f255b58b006"), EncryptedNote("543a31e9ac3e9381f36111a31458f09b16e88ce77393f61d01a4bc9ade7148779482f53e5c1543d1aeabfad77e53ec54d5455327d852a0b29789001a00e7b9dd04c63272a9db434b1b670423176d501347db17416c2eb59519b5bb533a78fb1105d77c4c2c29363611a5201de64a0146557e8a755038dbb363dd2e0fed121bfcb7f7c9ffa34a2cba85c8bf788bf0a3e64a8edc37e512afbb948d8ae1de1f45b3b4fc15342bdaf9268e4c047b76780201a9f3feae0f6a9601ed8a037ffa0e5cc2034b22f99bb3c67c4ec825b58af5a5289312db954e551499dadf3fe472b3f4c24f7b624c96a39c2eda039904cda6c59909065ceba1c28b6463181fb9700741aca782bf1803318be5b47791db973074709ba29ec328d3f2ab59e07fe08ef6da0b09a8e3f425500c91f9763082e71b350d14695a756e50841ffc5189ec2e5bfdfeb9383ff9ee1ff8ca1becfc243b817e9869541462ee3858258f90c4fd011017f09fcd6e22e096cb720589857a21123453a6223cc5b4c42fcea1c48c8db2a5c1b2a41546db9b9be6ab048753673122239e678a253f36806e630748ae8ce252d732c6ad31468151ee6ea3f371f9ba1d07b84d2b10235b1a49ff65b64d6a06ffd3ec0c2ef39a96bef9d73c1f524cdcb77c0acc49931989deabbae9137c30fb7a3461ea439b7783cb721492df78b5cc4f13ab511a73f326abeef08b1716521afe6d2cbffc23f64d6c5fc6e262ab1b98ecc1b8c862e65308b4a128ea8ea209d3937f6407a45c5cffa5c5ec7e94a12038ba9fc8ac9e724a3bec3c2027ae13146f6ce365f9c7f951293dc0018e6bb6f26e8079ba02229a57977a59b5e4")] }, rest: [JoinSplit { vpub_old: Amount(1692210556209567), vpub_new: Amount(1503578572708830), anchor: Root("db6511382235f4da9b9aef66b4ebe36e7d900f43483f80b619de8b6182c9c299"), nullifiers: [Nullifier([188, 123, 80, 144, 62, 52, 58, 116, 167, 232, 34, 51, 142, 240, 143, 96, 142, 99, 22, 155, 49, 45, 129, 225, 253, 105, 102, 45, 10, 144, 228, 135]), Nullifier([89, 127, 7, 24, 29, 136, 226, 56, 170, 15, 84, 120, 109, 253, 96, 91, 5, 213, 231, 111, 3, 204, 155, 198, 193, 116, 48, 36, 100, 121, 23, 112])], commitments: [NoteCommitment([17, 39, 148, 203, 187, 78, 83, 146, 251, 212, 132, 53, 127, 123, 177, 217, 112, 62, 38, 181, 219, 23, 78, 162, 0, 82, 125, 213, 15, 129, 35, 251]), NoteCommitment([153, 146, 80, 164, 219, 101, 249, 235, 177, 202, 6, 199, 81, 202, 105, 46, 230, 218, 148, 14, 34, 38, 211, 102, 12, 116, 68, 0, 92, 87, 32, 68])], ephemeral_key: PublicKey(MontgomeryPoint([93, 235, 186, 183, 117, 136, 182, 97, 127, 29, 196, 148, 199, 12, 172, 16, 132, 58, 197, 52, 255, 39, 143, 208, 183, 161, 14, 247, 203, 150, 9, 215])), random_seed: [165, 143, 7, 139, 26, 130, 199, 178, 60, 74, 184, 185, 0, 213, 200, 87, 97, 85, 230, 209, 207, 108, 105, 31, 65, 115, 253, 20, 214, 203, 44, 105], vmacs: [Mac([229, 62, 68, 172, 183, 99, 120, 148, 142, 228, 213, 236, 112, 208, 222, 197, 59, 132, 62, 137, 234, 44, 206, 165, 195, 38, 13, 86, 199, 60, 201, 3]), Mac([67, 129, 237, 153, 27, 242, 193, 24, 186, 77, 165, 194, 157, 40, 194, 6, 160, 61, 156, 13, 129, 89, 22, 238, 40, 209, 203, 141, 134, 155, 246, 213])], zkproof: Groth16Proof("c25c5af58c04648e7033c17200c139017c2682c6eb2aa818bf136e6b75c709fa3fa28fce992eb6aeb5fd073db9e4be34681722ec2c0899ea95015953347ca86fc1837563dd75c3b68e3d3dae3d03caf1902330cd3b3bca658dbef00f7ca47dd62570e4cb4803c9831a4a4e22ccbad4bb112f81dffbfa9017e20d03942dd503c8127c5a4b558005a53ae3ab4c90d54aaeaa5347852cf80074e33200d95a2d47b2c3e92b1715399d713c9cab68d05765c90eb5ef9d5c876068731edc7f34fef5f9"), enc_ciphertexts: [EncryptedNote("af82566bc237d85cb9e37d9aeb0be033b732529eb903046b11319b51ab62b37666dee317c2b0841bec06001afdb315a1097c85cf2d4d531e66104b31854aa8d7149730b77214d1e7e30913cd7cd339a0245d9431b54a38eeb5782d12fb7bc178325574985a605003cd96d42d16e75524fc3f984aefe7e1796c8e685eaf1faea72cfbe48bf37acdffaa6a944e12c69aaf12040659bd9406f0c70b0ed17ac193f499b858e341d40ab8dc953873e6a06b2f0f7f2cff4065a1aa5ebbdb991292f5cb04786a0021b974729859042d60a0912497c5b1555b51f3f6bf438633010552cdc0a290a1254822b5fba5ea2d5dac2e277779153fdbc126c796e2833f9118cd49c8a835e4ce32a5e93fdfa477ff4f110a1f7a6c8ade24589c7e8de8c8fabd2619ff2732b3cd924c41a533f932064723ed0988c6172aa573a85f045749dc0a49e8845ee1e7e83d9a58bd27c95d3d21dee728229956de0c6864cc59f36f4bd49ab1cc8c6b31421bac532e169672111dbf8331113e3f7595391fbae89b050e5dee6ac88ec63cfd26e4f7266946ad6655d1f8e9fcf3087bfb995522ce718e4f4bef2a94540e2269a85f01d5a0680a7c4bb99a5b7a398975625e930d3de45616c178510aa37dfb53f075d6345037b4888950de693f4f8b8a7921b336f9e9169d3410f92031ca14c5ed4bb17d2c4628c9bf3824b420520292e425e862d9d0c6a2a7450afb56fd869387de70580f2d3165ba76b948426c5513c5f2790bab8d69193ed3476f43afa6d1ff6eefbdba7252197d5e2f4c6a1cec182e0fc3335e802e5a35ec1fd27cd5c1f62dcae529a451ad0cf34f27037113ebaa08716770"), EncryptedNote("4ba6f098f17400bc1f9249fe5191fa673a20a4974d7604037843d4558dd35cd0f45ab5e2a50b61e6a29a89111ebf72c4c9aae2586fcb94867957db0e3df563466ac9502f2373ae45bd7b93354186bfb77885c073dd621abe2403a1419dcb148f9d9d7a87b9fdc06b7f8cace083018c7b1ca055dd6305ac4ef293cee3b71a943135bb4d7e2156f83c7799f968ebca25e60182d68ff75e203bfbab79cc2ddde15242cf5b523cc5e1aafa8b57bf70b1f3d6f6d3296acc5de66fd2e66d225e2047e8328bd0e2567840ecf35bcb74364d3b63fb9e3627cb1d7d6bb979cdf7c1a46008a1124abe879dbe7faa5065f160c0fc8e06e90fe50a12524f64ac10e70d86a7d748edb571f3cb6dfe7808501bae7354d706fdebd178e13420a1507e5926be605be174d82a7ea24d7d59ac8ee68371f6839ed415e7d63d5fa1ac7da46952094db5c13189889d8f90c7ff51b6bde957f5a4e85c52537f1dafb58427be15bb3b539dc4269a80ae6dc2bfbd7717443c2887678b5c43c2e73ac71bf1310041b812ae232f45852471cf3a1453ec09d81349983de73521431cbfd6e0215ba52a109fb5f0fb27238d1dbdda6c57d2820b9a8a5f88e0b60cfb001305dbe6686bbc839be6f63e7fa66ef9de26c65405f292698ed6a8ae0cdae9d298a8b00264417dec65679121abcf996bc7b3326fe7bef9bc2be8faf8c904f02dca1b60bdd6a2e1a7b6a3a2f548e437c9c2a88b3bb229f3d3c7c6f0d77136e8d0c15f806832633567643d563d04880819f1560d8a9764292e780e4ed499c37380c031ba97fd79452243a34f1de29ee0aed35b0713991b20fecde9b713a8b80707b829c59b")] }, JoinSplit { vpub_old: Amount(1137200131618708), vpub_new: Amount(1867620618777919), anchor: Root("d724c2ea28daa3000987dfa358efbe79a686a4361b04fe6379fc3bb4ce7b2f64"), nullifiers: [Nullifier([92, 125, 166, 14, 209, 195, 107, 160, 61, 99, 91, 134, 77, 217, 148, 7, 174, 76, 128, 95, 5, 186, 207, 193, 48, 39, 234, 137, 101, 188, 74, 49]), Nullifier([207, 87, 215, 214, 57, 34, 96, 32, 107, 174, 164, 232, 36, 160, 60, 100, 243, 68, 3, 79, 121, 92, 101, 127, 11, 196, 197, 62, 34, 91, 14, 220])], commitments: [NoteCommitment([154, 75, 11, 133, 209, 26, 76, 227, 107, 154, 84, 232, 218, 95, 66, 112, 66, 65, 101, 236, 150, 119, 1, 141, 209, 209, 166, 51, 152, 80, 229, 13]), NoteCommitment([169, 171, 102, 29, 148, 70, 174, 5, 76, 220, 186, 101, 142, 230, 62, 137, 137, 54, 123, 106, 70, 38, 170, 46, 31, 3, 208, 31, 166, 136, 72, 6])], ephemeral_key: PublicKey(MontgomeryPoint([243, 202, 254, 159, 148, 230, 187, 31, 228, 95, 123, 92, 156, 223, 106, 214, 130, 26, 82, 47, 164, 68, 186, 136, 67, 73, 231, 73, 35, 21, 97, 204])), random_seed: [78, 238, 36, 94, 236, 85, 94, 162, 105, 80, 187, 16, 178, 102, 32, 184, 113, 112, 233, 173, 94, 1, 44, 146, 162, 190, 128, 47, 231, 225, 240, 243], vmacs: [Mac([169, 113, 132, 194, 105, 61, 105, 162, 225, 194, 133, 110, 63, 25, 28, 98, 74, 130, 207, 36, 139, 128, 88, 157, 91, 4, 18, 104, 93, 71, 83, 23]), Mac([123, 69, 86, 69, 67, 35, 237, 159, 230, 23, 208, 121, 186, 193, 212, 147, 239, 76, 230, 46, 6, 40, 207, 89, 185, 124, 184, 26, 14, 226, 165, 115])], zkproof: Groth16Proof("097fb3583e6e185b0baa216996db08582edbf4288c91b4c5c82e3ec517409539080d79569d0604901383b6707848423e347247f93d096cc1ef082aeed7fc2f721c7613e3b21c980b1cfb91d137cc0f70b002f957b4f8358efb8cb931dfc990ad3e3b6a890071bcbe84b99fd205dd3445f67f859d794fd21fb4b05936b462b2d0bfd1a97d0e345e9d0f5cfca687d2afa87fdf769faa5281a30a406b7c160ee2cae5a73064533cfc06f770e24f0f6636ead64046bd7b060da7dc36df8e4255792b"), enc_ciphertexts: [EncryptedNote("a2dd994073556eb1e3a1a808e0d17d946c001f7fa109393b2dc3cabadc4b761a331401686b8355bb847dd7b5bdbccbc551d2387e255c4e7cc8b19e63eed1c4d53fe3b65b9ee397bafc45222113048f588c60d9b2a601344e044f293f5f106847bddacacaa567b375fd5558cd605343affbdef672081c8007d4ee67d0deffbe64e0eb9e2dfa08986814012cba9e4db6a063b055dbb5bc4afda524b2d11b667ffec9fe4109e69e25e4c6f382513d8d7a898b6209130d76a64bc29c86fa939349c51e0e957261a3973241abfe7eea7ed968aee4ee024bcccca24d7fbf84c997e772dbab26b7ce0425ded53ac1198d263d41bf0a2da9a3e4d9e85b2fa03fb8cdb3972a6cb9863523099697213090bd057d1cc62f62abc72b215f93d8a5dc2b7a4a73ec86450f65812a76d88d11db2d35a9aa8440e8d4afba1323de8975fd381560383434fb9865229591afd89116af1296233de158e289a8a6bb8ff8838511d67cb2e2cf722bad928f3ff6233737214d969775ee019a4e448a9d0af263a71d35cd7e2feebbefb03bdb026ec91314751674f58ad147e55078f5378dcc312a619faf957fb0cc633f3be8b643af1dec9b6e873c905923284f839192121fd424e19b52ba4fafb38e7a347cea706e0a8606eea152dd48f237d4208acd2ed9d39900e05e535cea054f73342cef3f686ebdf3524d571febbbeeba2c2edb5883b3c5c330309aa746e7210cc2339a94ff9eb3089acaadfb04d5a074360a88991a68b26dd30381de19e779e86dbd3e7e182fce4ce78cec3ae7201291a2a743416015408d765a766d7cd3104ca48b01a5193372676cbbf69c714a14f23934077f"), EncryptedNote("5d989f0279516a7a39c2b61a3adc4ebb128efebffcbebfacc9e932cb7604136c21dd0c1c8fa011217bda596f8060b481ef2f0ef6b9aef4b91954f22f1b10c57ced078d87cfdd7eba10d98a4eec1f1714a86135044fae0d6de8a1eae5d2a2590996fac87eb285b2af900c3e5dc798c2d371a7d11a449ff0e226e7ba9280cfb329aef0e2963854174d0fb31ccb8c9f541838b1c8a5fcced2eec78521bc3eb3d15f28e7d639a553402d0e0d7f717afd07be0bbe16af588e2fbe7908839a7ce61135e8ba36632c07570df973fa83395f9348c9fdc384a0191139ede26441644494bb47cfce76e88dbc9fb2524bbd16bcb8784a1250cecd34198324a2a747ae4c357206aeb58737dece19e64351935601890c353c7b3e03b582522b0d14651979842df3e446115d40ad14c69caa2b10972928dafcd5b42b2759db149ae7a840523e108a4e754369fd53ce04f35816b6970f9c0dd478973e000454955eefc82336d2d1350b8a732ed3a70db4884297a103faa59955783126b208e5687a618cb9c18841e34494aec2efca541ba0177579a200e3494a35aa34c7330a3128be1c40e5475f6a759d138ab19aec50c2e96e96ca0fe2ac7e1e8187d796decc5916da60f3c08f86c61a3edf956d65914e3e21a5d17544d159eebda380bad862dfed21a6882efafc2114020862d2067acbe48c7ce0ad4aa1597437a28d7f5ad528a22540c139432de63b1721a56bae0115b84152aea6d24faf9fe7b36019828301bc17026c6a5a9390a0cbe9708aba365f9d126175a86b040733889d68d0bab05462729f65e19bd893b78ae375f771e66f15912594868726126a49cbc7ce8fb8")] }, JoinSplit { vpub_old: Amount(9998551390637), vpub_new: Amount(24529126197192), anchor: Root("f6cffface793a250a61b686c7afa3142f751637da316274bb69fb06e584e86c8"), nullifiers: [Nullifier([226, 90, 188, 245, 181, 35, 26, 70, 36, 57, 213, 191, 194, 248, 239, 169, 80, 97, 179, 90, 75, 37, 193, 112, 0, 184, 151, 23, 53, 185, 176, 136]), Nullifier([166, 17, 76, 221, 185, 7, 215, 135, 235, 24, 206, 189, 183, 31, 92, 166, 77, 13, 161, 184, 196, 213, 68, 118, 186, 25, 153, 251, 40, 181, 159, 126])], commitments: [NoteCommitment([167, 96, 98, 15, 222, 30, 83, 39, 57, 43, 0, 248, 162, 252, 117, 165, 7, 124, 116, 51, 73, 113, 143, 239, 133, 26, 82, 64, 167, 53, 64, 162]), NoteCommitment([156, 164, 21, 57, 184, 186, 129, 150, 156, 57, 243, 223, 126, 41, 31, 227, 187, 18, 121, 70, 179, 148, 112, 23, 43, 114, 178, 189, 243, 165, 4, 161])], ephemeral_key: PublicKey(MontgomeryPoint([83, 56, 75, 162, 52, 140, 222, 161, 222, 57, 117, 52, 216, 232, 153, 130, 100, 206, 217, 237, 47, 23, 174, 106, 100, 139, 216, 186, 200, 153, 224, 59])), random_seed: [111, 69, 107, 40, 103, 166, 187, 64, 203, 177, 222, 14, 28, 205, 171, 126, 29, 30, 36, 31, 39, 85, 111, 23, 12, 222, 170, 241, 141, 254, 81, 141], vmacs: [Mac([44, 150, 83, 44, 35, 47, 158, 25, 9, 187, 38, 157, 233, 190, 27, 230, 66, 209, 48, 52, 38, 249, 117, 17, 125, 247, 233, 197, 85, 170, 163, 235]), Mac([230, 234, 220, 27, 73, 88, 56, 147, 180, 253, 205, 70, 106, 58, 93, 70, 44, 191, 141, 88, 247, 46, 23, 97, 247, 50, 185, 133, 5, 115, 4, 253])], zkproof: Groth16Proof("e99c6d7ce703959c9306ae4ad5b4ec3ae637878e6335aec21bef3b9abde024754f37fa0b55fb1a5d8441e1f434ed6306002df82c34bd185168de0ba116a2cbf4bc094163e282a586b549e6c80fbe7d65df0297fd342a91400dc4cca6f58692f11b5d28d7256cb1b842b800c36c28a9e396516a1993c216c05fc67266160d19fc3c98b350e6d2aea9d274af9ac2e46d501e79043cb0c0ba7d39d8e6275d6882698aed2ff7245e76c8a8702c7c07833c5f508ff5ed814dc76170842d0fc5bf7bc0"), enc_ciphertexts: [EncryptedNote("77e7778050ee967a21bf79025bda13cd0e60a67208fed68e048dde9352b0c8c446f69c208fbaaaa6673b01162ada6c8cff0295778df5b70c78f0c93d0059e5e720557b63e6a8cf3e5402802868cb2f91e3ae8c328ada84751bcf8e9c97bf80512abbdbbf94ba1bdc8961838851ba3b779c806696a0b9c98d83ae5123ff3a2e164c737423fb30e49fb928965d0d936b075f3e7c191a13f27a35e64c8f9529de2f846dff3ea8e031bc63e34331725dc272d011332016ebb56a64af7884831a467fe59928284a9bce25b60ba2bb5393ac92ed3d439fb1e567ceb4ac1f56af5c5e1bf5e0b30cf773dd8ec61c6fd319ad843a9f6f2c9ba65e16b3f357ad6b1167b222e351d0719a64ed880e7e74013740aa9e230dbec8b47220774600cc25afa16066d7351d5518b32544b9c67781ad8464339d9aba43a87922dcc9161ace6653a7c1200918152c06d7332426fcae5d05d0e2c2939748d9cfea3532d92cf8a11aab5862d921240ee537c8183621795807338e815bba2294f56685296e94fec56c1f26c7add125b60fae8f1fb4736830cf3e4d48d7961d8ecd8989ed42ee6355c90034e886b9cb3aa3546ca425a06b731ad05990ca6bfcf0dccd7920fa32a2fdf1034245adfb92d14217cbad952f97e2d2dc1269f8c5e55aa80bbe72ba05eefe6e8e14968cdc1d19544bd6fb340366827353af97c26a3dfea3707b4f9942e6f3df407a4a86beffd430e85786959fe20a480f2624656f0f61f995274b8f03050867aa32a07e4b97d93589745ca214cd5dd784e67a12ef240405bb09da9fec3f76211863f64a7898bc9cd41d3afbff1688f4efe96eb8f14ed547eb8057"), EncryptedNote("e5b3819a874e95c5c0a1f6c9564a82694b9479ceef670f3a9ed23a49840187a015beaa173c7f1ace8840760efeffd8c7e0f85bd3607cb8de0569071eb1b92cd042730a638341c0d2009d605352364951f5c669b1c7a5aa3ce9fdff3b0b7d3291de8595fbf6843607c1eb864c577ae69883fe26afbd0f2960eac8437b12c3a430c5e266b13aba17bd187ce7b97e6823fca1f4aafe1141c20da18cb3f2d1d4795244a65e73f1c747920cafb013e12c550b90183ee119b4c4473257443239cc7f2878f3b921ce57bb557adb37364f90789230b82641172c540610d682e376a32e969993f914efff329c15b819cec2dcd1724bb5044eaa7ca9e68d2678763dad40e2f25caf18dde3a7821fda8e1fcacf22a8a1925790173e8e28142b88d1706f5e43c79fd869574c088b19a76698920ed3ea55e812b84e168d451411b8681deb10f8980777c73196f0383d5b465f0fb49d871c13c169f7090c64bcf01284c7426e2714cd6670964cb65247319cf204590998ca9ee60fd2ae0d5d925dbe157bdd596ba81c0cd2b262028f41963b86b098c4ae78ae3032097b7e3cea334cce9ac910609f82393552b0a54d98578be9e27dfff0bf0ac3f6841705af41454bb72f0ecad07e21c859f1cf67cad781df23b6281d963e37998743c84956dc94bc6716a66c9ddf0d4f1e1ab285a7a7b7f3938fc52445d027f37afb45cf35b802d96f11d6a8b9de465161172e39b12c46b60e7d04e32a7cf4ef93d6ba7332c8e1a881df315dafffc7a130cc6cdd2b0d122a2a3f85fe273332264f65d0068c13d1205b7f6a4a462e0e23becba0e25f3769d8dd690c292db79698ca4a235beea5")] }], pub_key: VerificationKeyBytes("68144c2de7452c81c17572a05b89619095fb298fa918aa3b18d8fe4911fa0bb8"), sig: Signature { R_bytes: "9cd9ebf1871cb85beba0ef18775a6eb60f670a9013cdddb2b0adf29c0ca43c35", s_bytes: "29089a15130198e4a6b0fed84ed203d1dca03cb1c15fadbd669818d23cb814c9" } } +cc 3324a37cf8f1c38af8a6c0620102e518461cda03f3db5e45a3676531a7b3b7b7 # shrinks to output = zebra_chain::transparent::Output, mut input = zebra_chain::transparent::Input, spend_order = Greater diff --git a/zebra-state/src/lib.rs b/zebra-state/src/lib.rs index 0a75eb66b86..c57e74a831e 100644 --- a/zebra-state/src/lib.rs +++ b/zebra-state/src/lib.rs @@ -36,4 +36,4 @@ pub use error::{BoxError, CloneError, CommitBlockError, ValidateContextError}; pub use request::{FinalizedBlock, HashOrHeight, PreparedBlock, Request}; pub use response::Response; pub use service::init; -pub use utxo::{new_outputs, Utxo}; +pub use utxo::{new_outputs, OrderedUtxo, Utxo}; diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 1295533e3e8..1c18d2e7b08 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -5,7 +5,7 @@ use zebra_chain::{ transaction, transparent, }; -use crate::Utxo; +use crate::OrderedUtxo; // Allow *only* this unused import, so that rustdoc link resolution // will work with inline links. @@ -73,7 +73,7 @@ pub struct PreparedBlock { /// Note: although these transparent outputs are newly created, they may not /// be unspent, since a later transaction in a block can spend outputs of an /// earlier transaction. - pub new_outputs: HashMap, + pub new_outputs: HashMap, /// A precomputed list of the hashes of the transactions in this block. pub transaction_hashes: Vec, // TODO: add these parameters when we can compute anchors. @@ -98,7 +98,7 @@ pub struct FinalizedBlock { /// Note: although these transparent outputs are newly created, they may not /// be unspent, since a later transaction in a block can spend outputs of an /// earlier transaction. - pub(crate) new_outputs: HashMap, + pub(crate) new_outputs: HashMap, /// A precomputed list of the hashes of the transactions in this block. pub(crate) transaction_hashes: Vec, } diff --git a/zebra-state/src/service/finalized_state.rs b/zebra-state/src/service/finalized_state.rs index 5a310fd0e83..83af0ed3740 100644 --- a/zebra-state/src/service/finalized_state.rs +++ b/zebra-state/src/service/finalized_state.rs @@ -1,10 +1,12 @@ //! The primary implementation of the `zebra_state::Service` built upon rocksdb +mod check; mod disk_format; #[cfg(test)] mod tests; +use std::collections::HashSet; use std::{collections::HashMap, convert::TryInto, path::Path, sync::Arc}; use zebra_chain::transparent; @@ -39,6 +41,7 @@ pub struct FinalizedState { impl FinalizedState { pub fn new(config: &Config, network: Network) -> Self { let (path, db_options) = config.db_config(network); + let column_families = vec![ rocksdb::ColumnFamilyDescriptor::new("hash_by_height", db_options.clone()), rocksdb::ColumnFamilyDescriptor::new("height_by_hash", db_options.clone()), @@ -49,6 +52,7 @@ impl FinalizedState { rocksdb::ColumnFamilyDescriptor::new("sapling_nullifiers", db_options.clone()), rocksdb::ColumnFamilyDescriptor::new("orchard_nullifiers", db_options.clone()), ]; + let db_result = rocksdb::DB::open_cf_descriptors(&db_options, &path, column_families); let db = match db_result { @@ -227,6 +231,44 @@ impl FinalizedState { ); } + let spent_transparent_outpoints; + let revealed_sprout_nullifiers; + let revealed_sapling_nullifiers; + let revealed_orchard_nullifiers; + + // Consensus rule: transactions in the genesis block are ignored. + if block.header.previous_block_hash != GENESIS_PREVIOUS_BLOCK_HASH { + // Reject double-spends of transparent outputs and nullifers, where: + // - both are within this block, and + // - one is in this block, and the other was already committed to the finalized state. + + spent_transparent_outpoints = + check::transparent_double_spends(&self.db, utxo_by_outpoint, &block, &new_outputs)?; + + revealed_sprout_nullifiers = check::nullifier_double_spends( + &self.db, + sprout_nullifiers, + block.sprout_nullifiers().cloned().collect(), + )?; + + revealed_sapling_nullifiers = check::nullifier_double_spends( + &self.db, + sapling_nullifiers, + block.sapling_nullifiers().cloned().collect(), + )?; + + revealed_orchard_nullifiers = check::nullifier_double_spends( + &self.db, + orchard_nullifiers, + block.orchard_nullifiers().cloned().collect(), + )?; + } else { + spent_transparent_outpoints = HashSet::new(); + revealed_sprout_nullifiers = HashSet::new(); + revealed_sapling_nullifiers = HashSet::new(); + revealed_orchard_nullifiers = HashSet::new(); + } + // We use a closure so we can use an early return for control flow in // the genesis case let prepare_commit = || -> rocksdb::WriteBatch { @@ -246,17 +288,37 @@ impl FinalizedState { } // Index all new transparent outputs - for (outpoint, utxo) in new_outputs.into_iter() { - batch.zs_insert(utxo_by_outpoint, outpoint, utxo); + // + // Spends of outputs created by this block will be added here, + // then deleted in the next statement. + for (outpoint, ordered_utxo) in new_outputs.into_iter() { + batch.zs_insert(utxo_by_outpoint, outpoint, ordered_utxo.utxo); + } + + // Mark all transparent inputs as spent + // + // Deletes of missing keys and multiple deletes of the same key are ignored, + // so we must check for double-spends separately above. + for spent_outpoint in spent_transparent_outpoints { + batch.delete_cf(utxo_by_outpoint, spent_outpoint.as_bytes()); + } + + // Mark sprout, sapling and orchard nullifiers as spent + // + // Multiple inserts of the same key are ignored, + // so we must check for double-spends separately above. + for sprout_nullifier in revealed_sprout_nullifiers { + batch.zs_insert(sprout_nullifiers, sprout_nullifier, ()); + } + for sapling_nullifier in revealed_sapling_nullifiers { + batch.zs_insert(sapling_nullifiers, sapling_nullifier, ()); + } + for orchard_nullifier in revealed_orchard_nullifiers { + batch.zs_insert(orchard_nullifiers, orchard_nullifier, ()); } // Index each transaction, spent inputs, nullifiers - // TODO: move computation into FinalizedBlock as with transparent outputs - for (transaction_index, (transaction, transaction_hash)) in block - .transactions - .iter() - .zip(transaction_hashes.into_iter()) - .enumerate() + for (transaction_index, transaction_hash) in transaction_hashes.into_iter().enumerate() { let transaction_location = TransactionLocation { height, @@ -265,29 +327,6 @@ impl FinalizedState { .expect("no more than 4 billion transactions per block"), }; batch.zs_insert(tx_by_hash, transaction_hash, transaction_location); - - // Mark all transparent inputs as spent - for input in transaction.inputs() { - match input { - transparent::Input::PrevOut { outpoint, .. } => { - batch.delete_cf(utxo_by_outpoint, outpoint.as_bytes()); - } - // Coinbase inputs represent new coins, - // so there are no UTXOs to mark as spent. - transparent::Input::Coinbase { .. } => {} - } - } - - // Mark sprout, sapling and orchard nullifiers as spent - for sprout_nullifier in transaction.sprout_nullifiers() { - batch.zs_insert(sprout_nullifiers, sprout_nullifier, ()); - } - for sapling_nullifier in transaction.sapling_nullifiers() { - batch.zs_insert(sapling_nullifiers, sapling_nullifier, ()); - } - for orchard_nullifier in transaction.orchard_nullifiers() { - batch.zs_insert(orchard_nullifiers, orchard_nullifier, ()); - } } batch diff --git a/zebra-state/src/service/finalized_state/check.rs b/zebra-state/src/service/finalized_state/check.rs new file mode 100644 index 00000000000..cfcead04eb9 --- /dev/null +++ b/zebra-state/src/service/finalized_state/check.rs @@ -0,0 +1,141 @@ +//! Consensus rule checks for the finalized state. + +use std::{ + collections::{HashMap, HashSet}, + hash::Hash, +}; + +use itertools::Itertools; + +use rocksdb::ColumnFamily; +use zebra_chain::{ + block::Block, + transparent::{self, OutPoint}, +}; + +use crate::{BoxError, OrderedUtxo}; + +use super::disk_format::IntoDisk; + +/// Reject double-spends of transparent outputs: +/// - duplicate spends that are both in this block, +/// - spends of an output that hasn't been created yet, +/// (in linear chain and transaction order), and +/// - spends of an output that was spent by a previous block. +/// +/// Returns the unique set of spends in `block_spends`, +/// including any that spend outputs created by this block. +/// +/// "each output of a particular transaction +/// can only be used as an input once in the block chain. +/// Any subsequent reference is a forbidden double spend- +/// an attempt to spend the same satoshis twice." +/// +/// https://developer.bitcoin.org/devguide/block_chain.html#introduction +/// +/// "Any input within this block can spend an output which also appears in this block +/// (assuming the spend is otherwise valid). +/// However, the TXID corresponding to the output must be placed at some point +/// before the TXID corresponding to the input. +/// This ensures that any program parsing block chain transactions linearly +/// will encounter each output before it is used as an input." +/// +/// https://developer.bitcoin.org/reference/block_chain.html#merkle-trees +pub fn transparent_double_spends( + db: &rocksdb::DB, + cf: &ColumnFamily, + block: &Block, + new_outputs: &HashMap, +) -> Result, BoxError> { + let mut block_spends = HashSet::new(); + + for (spend_tx_index_in_block, transaction) in block.transactions.iter().enumerate() { + let spends = transaction.inputs().iter().filter_map(|input| match input { + transparent::Input::PrevOut { outpoint, .. } => Some(outpoint), + // Coinbase inputs represent new coins, + // so there are no UTXOs to mark as spent. + transparent::Input::Coinbase { .. } => None, + }); + + for spend in spends { + // check for in-block duplicate spends + if block_spends.contains(spend) { + return Err("duplicate transparent OutPoint spends within block".into()); + } + + // check spends occur in chain order + // + // because we are in the finalized state, there is a single chain of ordered blocks, + // so we just need to check spends within the same block, and the finalized UTXOs. + + if let Some(output) = new_outputs.get(spend) { + // reject the spend if it uses an output from this block, + // but the output was not created by an earlier transaction + // + // we know the spend is invalid, because transaction IDs are unique + if output.tx_index_in_block >= spend_tx_index_in_block { + return Err("spend of transparent OutPoint created in the current or later transaction in block ".into()); + } + } else { + // reject the spend if its UTXO is not available in the state or the block + if db.get_cf(cf, spend.as_bytes())?.is_none() { + return Err( + "no unspent output in previous block transaction or finalized state".into(), + ); + } + } + + block_spends.insert(*spend); + } + } + + Ok(block_spends) +} + +/// Reject double-spends of nullifers: +/// - both within this block, and +/// - one in this block, and the other already committed to the finalized state. +/// +/// `NullifierT` can be a sprout, sapling, or orchard nullifier. +/// +/// Returns the unique set of nullifiers in `block_reveals`. +/// +/// "A nullifier MUST NOT repeat either within a transaction, +/// or across transactions in a valid blockchain. +/// Sprout and Sapling and Orchard nullifiers are considered disjoint, +/// even if they have the same bit pattern." +/// +/// https:///zips.z.cash/protocol/protocol.pdf#nullifierset +/// +/// "A transaction is not valid if it would have added a nullifier +/// to the nullifier set that already exists in the set" +/// +/// https://zips.z.cash/protocol/protocol.pdf#commitmentsandnullifiers +pub fn nullifier_double_spends( + db: &rocksdb::DB, + cf: &ColumnFamily, + block_reveals: Vec, +) -> Result, BoxError> { + // check for in-block duplicates + let unique_spend_count = block_reveals.iter().unique().count(); + if unique_spend_count < block_reveals.len() { + return Err(format!( + "duplicate {} spends within block", + std::any::type_name::() + ) + .into()); + } + + // check for block-state duplicates + for spend in block_reveals.iter() { + if db.get_cf(cf, spend.as_bytes())?.is_some() { + return Err(format!( + "duplicate {} spend already committed to finalized state", + std::any::type_name::() + ) + .into()); + } + } + + Ok(block_reveals.into_iter().collect()) +} diff --git a/zebra-state/src/service/finalized_state/tests/prop.rs b/zebra-state/src/service/finalized_state/tests/prop.rs index e04094e94ec..3e18a2887c6 100644 --- a/zebra-state/src/service/finalized_state/tests/prop.rs +++ b/zebra-state/src/service/finalized_state/tests/prop.rs @@ -1,7 +1,20 @@ -use std::env; +//! Randomised property tests for the finalized state. -use zebra_chain::block::Height; -use zebra_test::prelude::*; +use std::{cmp::Ordering, collections::HashMap, env, sync::Arc}; + +use proptest::prelude::*; + +use zebra_chain::{ + block::{Block, Height}, + fmt::TypeNameToDebug, + parameters::Network::*, + primitives::Groth16Proof, + serialization::ZcashDeserializeInto, + sprout::JoinSplit, + transaction::{JoinSplitData, LockTime, Transaction}, + transparent::{self, OutPoint}, +}; +use zebra_test::prelude::Result; use crate::{ config::Config, @@ -9,11 +22,19 @@ use crate::{ arbitrary::PreparedChain, finalized_state::{FinalizedBlock, FinalizedState}, }, + OrderedUtxo, }; +use Ordering::*; + const DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES: u32 = 32; +/// Make sure blocks with v5 transactions are correctly committed or rejected +/// from the finalized state. #[test] +// TODO: move the double-spend checks to the non-finalized state / service, +// and delete the nullifier checks in this test +#[ignore] fn blocks_with_v5_transactions() -> Result<()> { zebra_test::init(); proptest!(ProptestConfig::with_cases(env::var("PROPTEST_CASES") @@ -23,15 +44,857 @@ fn blocks_with_v5_transactions() -> Result<()> { |((chain, count, network) in PreparedChain::default())| { let mut state = FinalizedState::new(&Config::ephemeral(), network); let mut height = Height(0); + let mut old_height = None; + + let mut sprout_nullifiers = HashMap::new(); + let mut sapling_nullifiers = HashMap::new(); + let mut orchard_nullifiers = HashMap::new(); + // use `count` to minimize test failures, so they are easier to diagnose for block in chain.iter().take(count) { - let hash = state.commit_finalized_direct(FinalizedBlock::from(block.clone())); - prop_assert_eq!(Some(height), state.finalized_tip_height()); - prop_assert_eq!(hash.unwrap(), block.hash); - // TODO: check that the nullifiers were correctly inserted (#2230) + for nullifier in block.block.transactions.iter().map(|transaction| transaction.sprout_nullifiers()).flatten() { + *sprout_nullifiers.entry(nullifier).or_insert(0) += 1; + } + + for nullifier in block.block.transactions.iter().map(|transaction| transaction.sapling_nullifiers()).flatten() { + *sapling_nullifiers.entry(nullifier).or_insert(0) += 1; + } + + for nullifier in block.block.transactions.iter().map(|transaction| transaction.orchard_nullifiers()).flatten() { + *orchard_nullifiers.entry(nullifier).or_insert(0) += 1; + } + + // TODO: detect duplicates in other column families, and make sure they are errors + + let commit_result = state.commit_finalized_direct(FinalizedBlock::from(block.clone())); + + // if there are any double-spends + if sprout_nullifiers.values().any(|spends| *spends > 1) || + sapling_nullifiers.values().any(|spends| *spends > 1) || + orchard_nullifiers.values().any(|spends| *spends > 1) { + prop_assert_eq!(old_height, state.finalized_tip_height()); + prop_assert!(commit_result.is_err()); + // once we've had an error, no more blocks can be committed, + // so skip the remainder of the test + prop_assume!(false); + } else { + prop_assert_eq!( + Some(height), + state.finalized_tip_height(), + "{:?}", + commit_result, + ); + prop_assert!(commit_result.is_ok()); + prop_assert_eq!(commit_result.unwrap(), block.hash); + } + + old_height = Some(height); height = Height(height.0 + 1); } }); Ok(()) } + +// These tests use the `Arbitrary` trait to easily generate complex types, +// then modify those types to cause an error. +// +// We could use mainnet or testnet blocks in these tests, +// but the differences shouldn't matter, +// because we're only interested in spend validation, +// (and passing various other state checks) +proptest! { + /// Make sure an arbitrary transparent output can be spent in the next block. + /// + /// This test makes sure there are no spurious rejections that might hide bugs in the other tests. + /// (And that the test infrastructure generally works.) + #[test] + fn accept_transparent_spend_in_next_block( + output in TypeNameToDebug::::arbitrary(), + // if we passed a height here, we'd get coinbase inputs, which are not spends + mut input in TypeNameToDebug::::arbitrary_with(None), + ) { + use transparent::Input::*; + + zebra_test::init(); + + let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into::>() + .expect("block should deserialize"); + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + let output_transaction = Transaction::V4 { + inputs: Vec::new(), + outputs: vec![output.0], + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + // create a valid spend + if let PrevOut { ref mut outpoint, ..} = &mut input.0 { + let unspent_outpoint = OutPoint { + hash: output_transaction.hash(), + index: 0, + }; + *outpoint = unspent_outpoint; + } else { + unreachable!("height: None should produce PrevOut inputs"); + } + + let spend_transaction = Transaction::V4 { + inputs: vec![input.0], + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + block1 + .transactions + .push(output_transaction.into()); + block2 + .transactions + .push(spend_transaction.into()); + + let mut state = FinalizedState::new(&Config::ephemeral(), Mainnet); + + prop_assert_eq!(None, state.finalized_tip_height()); + + let commit_result = state.commit_finalized_direct(FinalizedBlock::from(genesis)); + + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block1))); + + prop_assert_eq!(Some(Height(1)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block2))); + + prop_assert_eq!(Some(Height(2)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + } + + /// Make sure duplicate transparent output spends are rejected by the finalized state + /// if they come from different OutPoints in the same Transaction. + #[test] + fn reject_duplicate_transparent_spends_in_transaction( + output in TypeNameToDebug::::arbitrary(), + // if we passed a height here, we'd get coinbase inputs, which are not spends + mut input1 in TypeNameToDebug::::arbitrary_with(None), + mut input2 in TypeNameToDebug::::arbitrary_with(None), + ) { + use transparent::Input::*; + + zebra_test::init(); + + let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into::>() + .expect("block should deserialize"); + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + let output_transaction = Transaction::V4 { + inputs: Vec::new(), + outputs: vec![output.0], + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + // create a double-spend across two inputs + if let (PrevOut { outpoint: ref mut outpoint2, ..}, PrevOut { outpoint: ref mut outpoint1, .. } ) = (&mut input2.0, &mut input1.0) { + let unspent_outpoint = OutPoint { + hash: output_transaction.hash(), + index: 0, + }; + *outpoint1 = unspent_outpoint; + *outpoint2 = unspent_outpoint; + } else { + unreachable!("height: None should produce PrevOut inputs"); + } + + let spend_transaction = Transaction::V4 { + inputs: vec![input1.0, input2.0], + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + block1 + .transactions + .push(output_transaction.into()); + block2 + .transactions + .push(spend_transaction.into()); + + let mut state = FinalizedState::new(&Config::ephemeral(), Mainnet); + + prop_assert_eq!(None, state.finalized_tip_height()); + + let commit_result = state.commit_finalized_direct(FinalizedBlock::from(genesis)); + + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block1))); + + prop_assert_eq!(Some(Height(1)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block2))); + + // block was rejected + prop_assert_eq!(Some(Height(1)), state.finalized_tip_height()); + prop_assert!(commit_result.is_err()); + } + + /// Make sure duplicate transparent output spends are rejected by the finalized state + /// if they come from different Transactions in the same Block. + #[test] + fn reject_duplicate_transparent_spends_in_block( + output in TypeNameToDebug::::arbitrary(), + // if we passed a height here, we'd get coinbase inputs, which are not spends + mut input1 in TypeNameToDebug::::arbitrary_with(None), + mut input2 in TypeNameToDebug::::arbitrary_with(None), + ) { + use transparent::Input::*; + + zebra_test::init(); + + let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into::>() + .expect("block should deserialize"); + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + let output_transaction = Transaction::V4 { + inputs: Vec::new(), + outputs: vec![output.0], + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + // create a double-spend across two inputs + if let (PrevOut { outpoint: ref mut outpoint2, ..}, PrevOut { outpoint: ref mut outpoint1, .. } ) = (&mut input2.0, &mut input1.0) { + let unspent_outpoint = OutPoint { + hash: output_transaction.hash(), + index: 0, + }; + *outpoint1 = unspent_outpoint; + *outpoint2 = unspent_outpoint; + } else { + unreachable!("height: None should produce PrevOut inputs"); + } + + let spend_transaction1 = Transaction::V4 { + inputs: vec![input1.0], + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + let spend_transaction2 = Transaction::V4 { + inputs: vec![input2.0], + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + block1 + .transactions + .push(output_transaction.into()); + block2 + .transactions + .push(spend_transaction1.into()); + block2 + .transactions + .push(spend_transaction2.into()); + + let mut state = FinalizedState::new(&Config::ephemeral(), Mainnet); + + prop_assert_eq!(None, state.finalized_tip_height()); + + let commit_result = state.commit_finalized_direct(FinalizedBlock::from(genesis)); + + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block1))); + + prop_assert_eq!(Some(Height(1)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block2))); + + // block was rejected + prop_assert_eq!(Some(Height(1)), state.finalized_tip_height()); + prop_assert!(commit_result.is_err()); + } + + /// Make sure transparent output spends are rejected by the finalized state + /// if the corresponding UTXO has already been spent from the state. + #[test] + fn reject_duplicate_transparent_spends_in_chain( + output in TypeNameToDebug::::arbitrary(), + // if we passed a height here, we'd get coinbase inputs, which are not spends + mut input1 in TypeNameToDebug::::arbitrary_with(None), + mut input2 in TypeNameToDebug::::arbitrary_with(None), + ) { + use transparent::Input::*; + + zebra_test::init(); + + let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into::>() + .expect("block should deserialize"); + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + let mut block3 = zebra_test::vectors::BLOCK_MAINNET_3_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + let output_transaction = Transaction::V4 { + inputs: Vec::new(), + outputs: vec![output.0], + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + // create a double-spend across two inputs + if let (PrevOut { outpoint: ref mut outpoint2, ..}, PrevOut { outpoint: ref mut outpoint1, .. } ) = (&mut input2.0, &mut input1.0) { + let unspent_outpoint = OutPoint { + hash: output_transaction.hash(), + index: 0, + }; + *outpoint1 = unspent_outpoint; + *outpoint2 = unspent_outpoint; + } else { + unreachable!("height: None should produce PrevOut inputs"); + } + + let spend_transaction1 = Transaction::V4 { + inputs: vec![input1.0], + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + let spend_transaction2 = Transaction::V4 { + inputs: vec![input2.0], + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + block1 + .transactions + .push(output_transaction.into()); + block2 + .transactions + .push(spend_transaction1.into()); + block3 + .transactions + .push(spend_transaction2.into()); + + let mut state = FinalizedState::new(&Config::ephemeral(), Mainnet); + + prop_assert_eq!(None, state.finalized_tip_height()); + + let commit_result = state.commit_finalized_direct(FinalizedBlock::from(genesis)); + + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block1))); + + prop_assert_eq!(Some(Height(1)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block2))); + + prop_assert_eq!(Some(Height(2)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block3))); + + // block was rejected + prop_assert_eq!(Some(Height(2)), state.finalized_tip_height()); + prop_assert!(commit_result.is_err()); + } + + /// Make sure transparent output spends are rejected by the finalized state + /// if they spend an output in the same or later transaction in the block. + /// + /// And make sure earlier spends are accepted. + #[test] + fn transparent_spend_order_within_block( + output in TypeNameToDebug::::arbitrary(), + mut input in TypeNameToDebug::::arbitrary_with(None), + spend_order in any::(), + ) { + use transparent::Input::*; + + zebra_test::init(); + + let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into::>() + .expect("block should deserialize"); + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + let mut output_transaction = Transaction::V4 { + inputs: Vec::new(), + outputs: vec![output.0.clone()], + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + let mut unspent_outpoint = OutPoint { + hash: output_transaction.hash(), + index: 0, + }; + + // create a spend of the output + if let PrevOut { ref mut outpoint, ..} = &mut input.0 { + *outpoint = unspent_outpoint; + } else { + unreachable!("height: None should produce PrevOut inputs"); + } + + if spend_order == Equal { + // Modifying the inputs changes the transaction ID, + // so the input now points to an output that doesn't exist. + // + // We compensate for the ID change when we call the service. + if let Transaction::V4 { ref mut inputs, ..} = &mut output_transaction { + inputs.push(input.0); + } else { + unreachable!("should be Transaction::V4"); + } + + unspent_outpoint = OutPoint { + hash: output_transaction.hash(), + index: 0, + }; + + block1 + .transactions + .push(output_transaction.into()); + } else { + let spend_transaction = Transaction::V4 { + inputs: vec![input.0], + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + if spend_order == Less { + block1 + .transactions + .push(spend_transaction.into()); + block1 + .transactions + .push(output_transaction.into()); + } else { + // this is the only valid order + block1 + .transactions + .push(output_transaction.into()); + block1 + .transactions + .push(spend_transaction.into()); + } + } + + let mut state = FinalizedState::new(&Config::ephemeral(), Mainnet); + + prop_assert_eq!(None, state.finalized_tip_height()); + + let commit_result = state.commit_finalized_direct(FinalizedBlock::from(genesis)); + + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let mut block1 = FinalizedBlock::from(Arc::new(block1)); + + // Fake a new output with the modified transaction ID. + // We can't just update the input, because that would change the transaction ID again. + if spend_order == Equal { + // miner reward, founders reward, and the one we added + prop_assert_eq!(block1.new_outputs.len(), 3); + // coinbase and the one we added + prop_assert_eq!(block1.block.transactions.len(), 2); + + let new_utxo = OrderedUtxo::new(output.0, Height(1), false, 1); + block1.new_outputs.insert(unspent_outpoint, new_utxo); + } + + let commit_result = + state.commit_finalized_direct(block1); + + if spend_order == Greater { + prop_assert_eq!( + Some(Height(1)), + state.finalized_tip_height(), + "{:?}", + commit_result, + ); + prop_assert!(commit_result.is_ok()); + } else { + // block was rejected + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_err()); + } + } + + /// Make sure an arbitrary sprout nullifier is accepted by the finalized state. + /// + /// This test makes sure there are no spurious rejections that might hide bugs in the other tests. + /// (And that the test infrastructure generally works.) + #[test] + fn accept_distinct_arbitrary_sprout_nullifiers( + mut joinsplit in TypeNameToDebug::>::arbitrary(), + mut joinsplit_data in TypeNameToDebug::>::arbitrary(), + ) { + zebra_test::init(); + + let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into::>() + .expect("block should deserialize"); + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + // make sure the nullifiers are distinct + if joinsplit.nullifiers[1] == joinsplit.nullifiers[0] { + joinsplit.nullifiers[1].0[0] = 0x00; + joinsplit.nullifiers[0].0[0] = 0x01; + } + + // make sure there are no other duplicate nullifiers + joinsplit_data.first = joinsplit.0; + joinsplit_data.rest = Vec::new(); + + let transaction = Transaction::V4 { + inputs: Vec::new(), + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: Some(joinsplit_data.0), + sapling_shielded_data: None, + }; + + block1 + .transactions + .push(transaction.into()); + + let mut state = FinalizedState::new(&Config::ephemeral(), Mainnet); + + prop_assert_eq!(None, state.finalized_tip_height()); + + let commit_result = state.commit_finalized_direct(FinalizedBlock::from(genesis)); + + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block1))); + + // block was rejected + prop_assert_eq!(Some(Height(1)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + } + + /// Make sure duplicate sprout nullifiers are rejected by the finalized state + /// if they come from the same JoinSplit. + #[test] + fn reject_duplicate_sprout_nullifiers_in_joinsplit( + mut joinsplit in TypeNameToDebug::>::arbitrary(), + mut joinsplit_data in TypeNameToDebug::>::arbitrary(), + ) { + zebra_test::init(); + + let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into::>() + .expect("block should deserialize"); + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + // create a double-spend within the same joinsplit + // this might not actually be valid under the consensus rules + joinsplit.nullifiers[1] = joinsplit.nullifiers[0]; + + joinsplit_data.rest.push(joinsplit.0); + + let transaction = Transaction::V4 { + inputs: Vec::new(), + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: Some(joinsplit_data.0), + sapling_shielded_data: None, + }; + + block1 + .transactions + .push(transaction.into()); + + let mut state = FinalizedState::new(&Config::ephemeral(), Mainnet); + + prop_assert_eq!(None, state.finalized_tip_height()); + + let commit_result = state.commit_finalized_direct(FinalizedBlock::from(genesis)); + + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block1))); + + // block was rejected + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_err()); + } + + /// Make sure duplicate sprout nullifiers are rejected by the finalized state + /// if they come from different JoinSplits in the same JoinSplitData/Transaction. + #[test] + fn reject_duplicate_sprout_nullifiers_in_transaction( + joinsplit1 in TypeNameToDebug::>::arbitrary(), + mut joinsplit2 in TypeNameToDebug::>::arbitrary(), + mut joinsplit_data in TypeNameToDebug::>::arbitrary(), + ) { + zebra_test::init(); + + let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into::>() + .expect("block should deserialize"); + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + // create a double-spend across two joinsplits + joinsplit2.nullifiers[0] = joinsplit1.nullifiers[0]; + + joinsplit_data.rest.push(joinsplit1.0); + joinsplit_data.rest.push(joinsplit2.0); + + let transaction = Transaction::V4 { + inputs: Vec::new(), + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: Some(joinsplit_data.0), + sapling_shielded_data: None, + }; + + block1 + .transactions + .push(transaction.into()); + + let mut state = FinalizedState::new(&Config::ephemeral(), Mainnet); + + prop_assert_eq!(None, state.finalized_tip_height()); + + let commit_result = state.commit_finalized_direct(FinalizedBlock::from(genesis)); + + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block1))); + + // block was rejected + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_err()); + } + + /// Make sure duplicate sprout nullifiers are rejected by the finalized state + /// if they come from different transactions in the same block. + #[test] + fn reject_duplicate_sprout_nullifiers_in_block( + joinsplit1 in TypeNameToDebug::>::arbitrary(), + mut joinsplit2 in TypeNameToDebug::>::arbitrary(), + mut joinsplit_data1 in TypeNameToDebug::>::arbitrary(), + mut joinsplit_data2 in TypeNameToDebug::>::arbitrary(), + ) { + zebra_test::init(); + + let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into::>() + .expect("block should deserialize"); + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + // create a double-spend across two transactions + joinsplit2.nullifiers[0] = joinsplit1.nullifiers[0]; + + joinsplit_data1.rest.push(joinsplit1.0); + joinsplit_data2.rest.push(joinsplit2.0); + + let transaction1 = Transaction::V4 { + inputs: Vec::new(), + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: Some(joinsplit_data1.0), + sapling_shielded_data: None, + }; + let transaction2 = Transaction::V4 { + inputs: Vec::new(), + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: Some(joinsplit_data2.0), + sapling_shielded_data: None, + }; + + block1 + .transactions + .push(transaction1.into()); + block1 + .transactions + .push(transaction2.into()); + + let mut state = FinalizedState::new(&Config::ephemeral(), Mainnet); + + prop_assert_eq!(None, state.finalized_tip_height()); + + let commit_result = state.commit_finalized_direct(FinalizedBlock::from(genesis)); + + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block1))); + + // block was rejected + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_err()); + } + + /// Make sure duplicate sprout nullifiers are rejected by the finalized state + /// if they come from different blocks in the same chain. + #[test] + fn reject_duplicate_sprout_nullifiers_in_chain( + joinsplit1 in TypeNameToDebug::>::arbitrary(), + mut joinsplit2 in TypeNameToDebug::>::arbitrary(), + mut joinsplit_data1 in TypeNameToDebug::>::arbitrary(), + mut joinsplit_data2 in TypeNameToDebug::>::arbitrary(), + ) { + zebra_test::init(); + + let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into::>() + .expect("block should deserialize"); + let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES + .zcash_deserialize_into::() + .expect("block should deserialize"); + + // create a double-spend across two blocks + joinsplit2.nullifiers[0] = joinsplit1.nullifiers[0]; + + joinsplit_data1.rest.push(joinsplit1.0); + joinsplit_data2.rest.push(joinsplit2.0); + + let transaction1 = Transaction::V4 { + inputs: Vec::new(), + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: Some(joinsplit_data1.0), + sapling_shielded_data: None, + }; + let transaction2 = Transaction::V4 { + inputs: Vec::new(), + outputs: Vec::new(), + lock_time: LockTime::min_lock_time(), + expiry_height: Height(0), + joinsplit_data: Some(joinsplit_data2.0), + sapling_shielded_data: None, + }; + + block1 + .transactions + .push(transaction1.into()); + block2 + .transactions + .push(transaction2.into()); + + let mut state = FinalizedState::new(&Config::ephemeral(), Mainnet); + + prop_assert_eq!(None, state.finalized_tip_height()); + + let commit_result = state.commit_finalized_direct(FinalizedBlock::from(genesis)); + + prop_assert_eq!(Some(Height(0)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block1))); + + prop_assert_eq!(Some(Height(1)), state.finalized_tip_height()); + prop_assert!(commit_result.is_ok()); + + let commit_result = + state.commit_finalized_direct(FinalizedBlock::from(Arc::new(block2))); + + // block was rejected + prop_assert_eq!(Some(Height(1)), state.finalized_tip_height()); + prop_assert!(commit_result.is_err()); + } +} diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index 522962689c7..f7cced66e08 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -155,8 +155,8 @@ impl NonFinalizedState { /// `transparent::OutPoint` if it is present in any chain. pub fn any_utxo(&self, outpoint: &transparent::OutPoint) -> Option { for chain in self.chain_set.iter().rev() { - if let Some(output) = chain.created_utxos.get(outpoint) { - return Some(output.clone()); + if let Some(ordered_utxo) = chain.created_utxos.get(outpoint) { + return Some(ordered_utxo.utxo.clone()); } } diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 5315ad1e079..84deca3fd04 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -10,7 +10,7 @@ use zebra_chain::{ transaction::Transaction::*, transparent, work::difficulty::PartialCumulativeWork, }; -use crate::{PreparedBlock, Utxo, ValidateContextError}; +use crate::{OrderedUtxo, PreparedBlock, ValidateContextError}; #[derive(Default, Clone)] pub struct Chain { @@ -18,7 +18,7 @@ pub struct Chain { pub height_by_hash: HashMap, pub tx_by_hash: HashMap, - pub created_utxos: HashMap, + pub created_utxos: HashMap, spent_utxos: HashSet, // TODO: add sprout, sapling and orchard anchors (#1320) sprout_anchors: HashSet, @@ -301,17 +301,17 @@ impl UpdateWith for Chain { } } -impl UpdateWith> for Chain { +impl UpdateWith> for Chain { fn update_chain_state_with( &mut self, - utxos: &HashMap, + utxos: &HashMap, ) -> Result<(), ValidateContextError> { self.created_utxos .extend(utxos.iter().map(|(k, v)| (*k, v.clone()))); Ok(()) } - fn revert_chain_state_with(&mut self, utxos: &HashMap) { + fn revert_chain_state_with(&mut self, utxos: &HashMap) { self.created_utxos .retain(|outpoint, _| !utxos.contains_key(outpoint)); } diff --git a/zebra-state/src/service/non_finalized_state/queued_blocks.rs b/zebra-state/src/service/non_finalized_state/queued_blocks.rs index d42c7d69d56..e55b4c71e69 100644 --- a/zebra-state/src/service/non_finalized_state/queued_blocks.rs +++ b/zebra-state/src/service/non_finalized_state/queued_blocks.rs @@ -6,7 +6,7 @@ use std::{ use tracing::instrument; use zebra_chain::{block, transparent}; -use crate::{service::QueuedBlock, Utxo}; +use crate::{service::QueuedBlock, OrderedUtxo, Utxo}; /// A queue of blocks, awaiting the arrival of parent blocks. #[derive(Default)] @@ -18,7 +18,7 @@ pub struct QueuedBlocks { /// Hashes from `queued_blocks`, indexed by block height. by_height: BTreeMap>, /// Known UTXOs. - known_utxos: HashMap, + known_utxos: HashMap, } impl QueuedBlocks { @@ -151,7 +151,9 @@ impl QueuedBlocks { /// Try to look up this UTXO in any queued block. pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option { - self.known_utxos.get(outpoint).cloned() + self.known_utxos + .get(outpoint) + .map(|ordered_utxo| ordered_utxo.utxo.clone()) } } diff --git a/zebra-state/src/service/pending_utxos.rs b/zebra-state/src/service/pending_utxos.rs index 277173f7b4a..cf8dfb6ffe4 100644 --- a/zebra-state/src/service/pending_utxos.rs +++ b/zebra-state/src/service/pending_utxos.rs @@ -5,7 +5,9 @@ use tokio::sync::broadcast; use zebra_chain::transparent; -use crate::{BoxError, Response, Utxo}; +use crate::OrderedUtxo; +use crate::Utxo; +use crate::{BoxError, Response}; #[derive(Debug, Default)] pub struct PendingUtxos(HashMap>); @@ -47,12 +49,9 @@ impl PendingUtxos { } /// Check the list of pending UTXO requests against the supplied UTXO index. - pub fn check_against(&mut self, utxos: &HashMap) { - for (outpoint, utxo) in utxos.iter() { - if let Some(sender) = self.0.remove(outpoint) { - tracing::trace!(?outpoint, "found pending UTXO"); - let _ = sender.send(utxo.clone()); - } + pub fn check_against(&mut self, ordered_utxos: &HashMap) { + for (outpoint, ordered_utxo) in ordered_utxos.iter() { + self.respond(outpoint, ordered_utxo.utxo.clone()) } } diff --git a/zebra-state/src/utxo.rs b/zebra-state/src/utxo.rs index 65c43f5cb41..d2eba4c585d 100644 --- a/zebra-state/src/utxo.rs +++ b/zebra-state/src/utxo.rs @@ -22,29 +22,72 @@ pub struct Utxo { pub from_coinbase: bool, } +/// A [`Utxo`], and the index of its transaction within its block. +/// +/// This extra index is used to check that spends come after outputs, +/// when a new output and its spend are both in the same block. +/// +/// The extra index is only used for in-block checks, +/// so it does not need to be stored in the state. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr( + any(test, feature = "proptest-impl"), + derive(proptest_derive::Arbitrary) +)] +pub struct OrderedUtxo { + /// An unspent transaction output. + pub utxo: Utxo, + /// The index of the transaction that created the output, in the block at `height`. + /// + /// Used to make sure that transaction can only spend outputs + /// that were created earlier in the chain. + /// + /// Note: this is different from `OutPoint.index`, + /// which is the index of the output in its transaction. + pub tx_index_in_block: usize, +} + +impl OrderedUtxo { + /// Create a new ordered UTXO from its fields. + pub fn new( + output: transparent::Output, + height: block::Height, + from_coinbase: bool, + tx_index_in_block: usize, + ) -> OrderedUtxo { + let utxo = Utxo { + output, + height, + from_coinbase, + }; + + OrderedUtxo { + utxo, + tx_index_in_block, + } + } +} + /// Compute an index of newly created transparent outputs, given a block and a /// list of precomputed transaction hashes. pub fn new_outputs( block: &Block, transaction_hashes: &[transaction::Hash], -) -> HashMap { +) -> HashMap { let mut new_outputs = HashMap::default(); let height = block.coinbase_height().expect("block has coinbase height"); - for (transaction, hash) in block + for (tx_index_in_block, (transaction, hash)) in block .transactions .iter() .zip(transaction_hashes.iter().cloned()) + .enumerate() { let from_coinbase = transaction.is_coinbase(); for (index, output) in transaction.outputs().iter().cloned().enumerate() { let index = index as u32; new_outputs.insert( transparent::OutPoint { hash, index }, - Utxo { - output, - height, - from_coinbase, - }, + OrderedUtxo::new(output, height, from_coinbase, tx_index_in_block), ); } }