diff --git a/Cargo.lock b/Cargo.lock index 417de7fe26..d88d1524b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3002,6 +3002,7 @@ dependencies = [ "f4jumble", "proptest", "zcash_encoding", + "zcash_protocol", ] [[package]] @@ -3046,6 +3047,7 @@ dependencies = [ "zcash_note_encryption", "zcash_primitives", "zcash_proofs", + "zcash_protocol", "zip32", ] @@ -3146,6 +3148,7 @@ dependencies = [ "zcash_address", "zcash_encoding", "zcash_primitives", + "zcash_protocol", "zip32", ] @@ -3200,6 +3203,7 @@ dependencies = [ "zcash_address", "zcash_encoding", "zcash_note_encryption", + "zcash_protocol", "zcash_spec", "zip32", ] @@ -3228,6 +3232,16 @@ dependencies = [ "zcash_primitives", ] +[[package]] +name = "zcash_protocol" +version = "0.0.0" +dependencies = [ + "document-features", + "incrementalmerkletree", + "memuse", + "proptest", +] + [[package]] name = "zcash_spec" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3049022446..df119c2430 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "components/f4jumble", "components/zcash_address", "components/zcash_encoding", + "components/zcash_protocol", "zcash_client_backend", "zcash_client_sqlite", "zcash_extensions", @@ -32,6 +33,8 @@ zcash_address = { version = "0.3", path = "components/zcash_address" } zcash_client_backend = { version = "0.11", path = "zcash_client_backend" } zcash_encoding = { version = "0.2", path = "components/zcash_encoding" } zcash_keys = { version = "0.1", path = "zcash_keys" } +zcash_protocol = { version = "0.0", path = "components/zcash_protocol" } + zcash_note_encryption = "0.4" zcash_primitives = { version = "0.14", path = "zcash_primitives", default-features = false } zcash_proofs = { version = "0.14", path = "zcash_proofs", default-features = false } diff --git a/components/zcash_address/Cargo.toml b/components/zcash_address/Cargo.toml index b797ae22d8..e8ed075a56 100644 --- a/components/zcash_address/Cargo.toml +++ b/components/zcash_address/Cargo.toml @@ -22,7 +22,8 @@ rustdoc-args = ["--cfg", "docsrs"] bech32 = "0.9" bs58 = { version = "0.5", features = ["check"] } f4jumble = { version = "0.1", path = "../f4jumble" } -zcash_encoding = { version = "0.2", path = "../zcash_encoding" } +zcash_protocol.workspace = true +zcash_encoding.workspace = true [dev-dependencies] assert_matches = "1.3.0" diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index 9e5e422ce6..2f5bf8445f 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -1,9 +1,11 @@ use std::{convert::TryInto, error::Error, fmt, str::FromStr}; use bech32::{self, FromBase32, ToBase32, Variant}; +use zcash_protocol::consensus::{NetworkConstants, NetworkType}; +use zcash_protocol::constants::{mainnet, regtest, testnet}; use crate::kind::unified::Encoding; -use crate::{kind::*, AddressKind, Network, ZcashAddress}; +use crate::{kind::*, AddressKind, ZcashAddress}; /// An error while attempting to parse a string as a Zcash address. #[derive(Debug, PartialEq, Eq)] @@ -68,9 +70,9 @@ impl FromStr for ZcashAddress { let data = Vec::::from_base32(&data).map_err(|_| ParseError::InvalidEncoding)?; let net = match hrp.as_str() { - sapling::MAINNET => Network::Main, - sapling::TESTNET => Network::Test, - sapling::REGTEST => Network::Regtest, + mainnet::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Main, + testnet::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Test, + regtest::HRP_SAPLING_PAYMENT_ADDRESS => NetworkType::Regtest, // We will not define new Bech32 address encodings. _ => { return Err(ParseError::NotZcash); @@ -86,23 +88,33 @@ impl FromStr for ZcashAddress { // The rest use Base58Check. if let Ok(decoded) = bs58::decode(s).with_check(None).into_vec() { - let net = match decoded[..2].try_into().unwrap() { - sprout::MAINNET | p2pkh::MAINNET | p2sh::MAINNET => Network::Main, - sprout::TESTNET | p2pkh::TESTNET | p2sh::TESTNET => Network::Test, - // We will not define new Base58Check address encodings. - _ => return Err(ParseError::NotZcash), - }; + if decoded.len() >= 2 { + let (prefix, net) = match decoded[..2].try_into().unwrap() { + prefix @ (mainnet::B58_PUBKEY_ADDRESS_PREFIX + | mainnet::B58_SCRIPT_ADDRESS_PREFIX + | mainnet::B58_SPROUT_ADDRESS_PREFIX) => (prefix, NetworkType::Main), + prefix @ (testnet::B58_PUBKEY_ADDRESS_PREFIX + | testnet::B58_SCRIPT_ADDRESS_PREFIX + | testnet::B58_SPROUT_ADDRESS_PREFIX) => (prefix, NetworkType::Test), + // We will not define new Base58Check address encodings. + _ => return Err(ParseError::NotZcash), + }; - return match decoded[..2].try_into().unwrap() { - sprout::MAINNET | sprout::TESTNET => { - decoded[2..].try_into().map(AddressKind::Sprout) + return match prefix { + mainnet::B58_SPROUT_ADDRESS_PREFIX | testnet::B58_SPROUT_ADDRESS_PREFIX => { + decoded[2..].try_into().map(AddressKind::Sprout) + } + mainnet::B58_PUBKEY_ADDRESS_PREFIX | testnet::B58_PUBKEY_ADDRESS_PREFIX => { + decoded[2..].try_into().map(AddressKind::P2pkh) + } + mainnet::B58_SCRIPT_ADDRESS_PREFIX | testnet::B58_SCRIPT_ADDRESS_PREFIX => { + decoded[2..].try_into().map(AddressKind::P2sh) + } + _ => unreachable!(), } - p2pkh::MAINNET | p2pkh::TESTNET => decoded[2..].try_into().map(AddressKind::P2pkh), - p2sh::MAINNET | p2sh::TESTNET => decoded[2..].try_into().map(AddressKind::P2sh), - _ => unreachable!(), + .map_err(|_| ParseError::InvalidEncoding) + .map(|kind| ZcashAddress { kind, net }); } - .map_err(|_| ParseError::InvalidEncoding) - .map(|kind| ZcashAddress { kind, net }); }; // If it's not valid Bech32, Bech32m, or Base58Check, it's not a Zcash address. @@ -124,36 +136,13 @@ fn encode_b58(prefix: [u8; 2], data: &[u8]) -> String { impl fmt::Display for ZcashAddress { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let encoded = match &self.kind { - AddressKind::Sprout(data) => encode_b58( - match self.net { - Network::Main => sprout::MAINNET, - Network::Test | Network::Regtest => sprout::TESTNET, - }, - data, - ), - AddressKind::Sapling(data) => encode_bech32( - match self.net { - Network::Main => sapling::MAINNET, - Network::Test => sapling::TESTNET, - Network::Regtest => sapling::REGTEST, - }, - data, - ), + AddressKind::Sprout(data) => encode_b58(self.net.b58_sprout_address_prefix(), data), + AddressKind::Sapling(data) => { + encode_bech32(self.net.hrp_sapling_payment_address(), data) + } AddressKind::Unified(addr) => addr.encode(&self.net), - AddressKind::P2pkh(data) => encode_b58( - match self.net { - Network::Main => p2pkh::MAINNET, - Network::Test | Network::Regtest => p2pkh::TESTNET, - }, - data, - ), - AddressKind::P2sh(data) => encode_b58( - match self.net { - Network::Main => p2sh::MAINNET, - Network::Test | Network::Regtest => p2sh::TESTNET, - }, - data, - ), + AddressKind::P2pkh(data) => encode_b58(self.net.b58_pubkey_address_prefix(), data), + AddressKind::P2sh(data) => encode_b58(self.net.b58_script_address_prefix(), data), }; write!(f, "{}", encoded) } @@ -162,7 +151,7 @@ impl fmt::Display for ZcashAddress { #[cfg(test)] mod tests { use super::*; - use crate::kind::unified; + use crate::{kind::unified, Network}; fn encoding(encoded: &str, decoded: ZcashAddress) { assert_eq!(decoded.to_string(), encoded); diff --git a/components/zcash_address/src/kind.rs b/components/zcash_address/src/kind.rs index 5397c027f8..38b4557a6e 100644 --- a/components/zcash_address/src/kind.rs +++ b/components/zcash_address/src/kind.rs @@ -1,7 +1 @@ pub mod unified; - -pub(crate) mod sapling; -pub(crate) mod sprout; - -pub(crate) mod p2pkh; -pub(crate) mod p2sh; diff --git a/components/zcash_address/src/kind/p2pkh.rs b/components/zcash_address/src/kind/p2pkh.rs deleted file mode 100644 index a37377d3c5..0000000000 --- a/components/zcash_address/src/kind/p2pkh.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// The prefix for a Base58Check-encoded mainnet transparent P2PKH address. -pub(crate) const MAINNET: [u8; 2] = [0x1c, 0xb8]; - -/// The prefix for a Base58Check-encoded testnet transparent P2PKH address. -pub(crate) const TESTNET: [u8; 2] = [0x1d, 0x25]; diff --git a/components/zcash_address/src/kind/p2sh.rs b/components/zcash_address/src/kind/p2sh.rs deleted file mode 100644 index 1ebef52ab1..0000000000 --- a/components/zcash_address/src/kind/p2sh.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// The prefix for a Base58Check-encoded mainnet transparent P2SH address. -pub(crate) const MAINNET: [u8; 2] = [0x1c, 0xbd]; - -/// The prefix for a Base58Check-encoded testnet transparent P2SH address. -pub(crate) const TESTNET: [u8; 2] = [0x1c, 0xba]; diff --git a/components/zcash_address/src/kind/sapling.rs b/components/zcash_address/src/kind/sapling.rs deleted file mode 100644 index 6f2e945b94..0000000000 --- a/components/zcash_address/src/kind/sapling.rs +++ /dev/null @@ -1,20 +0,0 @@ -/// The HRP for a Bech32-encoded mainnet Sapling address. -/// -/// Defined in the [Zcash Protocol Specification section 5.6.4][saplingpaymentaddrencoding]. -/// -/// [saplingpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#saplingpaymentaddrencoding -pub(crate) const MAINNET: &str = "zs"; - -/// The HRP for a Bech32-encoded testnet Sapling address. -/// -/// Defined in the [Zcash Protocol Specification section 5.6.4][saplingpaymentaddrencoding]. -/// -/// [saplingpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#saplingpaymentaddrencoding -pub(crate) const TESTNET: &str = "ztestsapling"; - -/// The HRP for a Bech32-encoded regtest Sapling address. -/// -/// It is defined in [the `zcashd` codebase]. -/// -/// [the `zcashd` codebase]: https://github.com/zcash/zcash/blob/128d863fb8be39ee294fda397c1ce3ba3b889cb2/src/chainparams.cpp#L493 -pub(crate) const REGTEST: &str = "zregtestsapling"; diff --git a/components/zcash_address/src/kind/sprout.rs b/components/zcash_address/src/kind/sprout.rs deleted file mode 100644 index 06a8a03c79..0000000000 --- a/components/zcash_address/src/kind/sprout.rs +++ /dev/null @@ -1,13 +0,0 @@ -/// The prefix for a Base58Check-encoded mainnet Sprout address. -/// -/// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. -/// -/// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding -pub(crate) const MAINNET: [u8; 2] = [0x16, 0x9a]; - -/// The prefix for a Base58Check-encoded testnet Sprout address. -/// -/// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. -/// -/// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding -pub(crate) const TESTNET: [u8; 2] = [0x16, 0xb6]; diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index a16281943e..6f516a941e 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -141,6 +141,7 @@ pub use convert::{ }; pub use encoding::ParseError; pub use kind::unified; +pub use zcash_protocol::consensus::NetworkType as Network; /// A Zcash address. #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -149,20 +150,6 @@ pub struct ZcashAddress { kind: AddressKind, } -/// The Zcash network for which an address is encoded. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Network { - /// Zcash Mainnet. - Main, - /// Zcash Testnet. - Test, - /// Private integration / regression testing, used in `zcashd`. - /// - /// For some address types there is no distinction between test and regtest encodings; - /// those will always be parsed as `Network::Test`. - Regtest, -} - /// Known kinds of Zcash addresses. #[derive(Clone, Debug, PartialEq, Eq, Hash)] enum AddressKind { diff --git a/components/zcash_protocol/CHANGELOG.md b/components/zcash_protocol/CHANGELOG.md new file mode 100644 index 0000000000..839e3b0845 --- /dev/null +++ b/components/zcash_protocol/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog +All notable changes to this library will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this library adheres to Rust's notion of +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +The entries below are relative to the `zcash_primitives` crate as of the tag +`zcash_primitives-0.14.0`. + +### Added +- The following modules have been extracted from `zcash_primitives` and + moved to this crate: + - `consensus` + - `constants` + - `zcash_protocol::value` replaces `zcash_primitives::transaction::components::amount` +- `zcash_protocol::consensus`: + - `NetworkConstants` has been extracted from the `Parameters` trait. Relative to the + state prior to the extraction, the bech32 prefixes now return `&'static str` instead + of `&str`. + - `NetworkType` + - `Parameters::b58_sprout_address_prefix` +- `zcash_protocol::consensus`: + - `impl Hash for LocalNetwork` +- `zcash_protocol::constants::{mainnet, testnet}::B58_SPROUT_ADDRESS_PREFIX` +- Added in `zcash_protocol::value`: + - `Zatoshis` + - `ZatBalance` + - `MAX_BALANCE` has been added to replace previous instances where + `zcash_protocol::value::MAX_MONEY` was used as a signed value. + +### Changed +- `zcash_protocol::value::COIN` has been changed from an `i64` to a `u64` +- `zcash_protocol::value::MAX_MONEY` has been changed from an `i64` to a `u64` +- `zcash_protocol::consensus::Parameters` has been split into two traits, with + the newly added `NetworkConstants` trait providing all network constant + accessors. Also, the `address_network` method has been replaced with a new + `network_type` method that serves the same purpose. A blanket impl of + `NetworkConstants` is provided for all types that implement `Parameters`, + so call sites for methods that have moved to `NetworkConstants` should + remain unchanged (though they may require an additional `use` statement.) + +### Removed +- From `zcash_protocol::value`: + - `NonNegativeAmount` (use `Zatoshis` instead.) + - `Amount` (use `ZatBalance` instead.) + - The following conversions have been removed relative to `zcash_primitives-0.14.0`, + as `zcash_protocol` does not depend on the `orchard` or `sapling-crypto` crates. + - `From for orchard::NoteValue>` + - `TryFrom for Amount` + - `From for sapling::value::NoteValue>` + - `TryFrom for NonNegativeAmount` + - `impl AddAssign for NonNegativeAmount` + - `impl SubAssign for NonNegativeAmount` diff --git a/components/zcash_protocol/Cargo.toml b/components/zcash_protocol/Cargo.toml new file mode 100644 index 0000000000..45d2c53e63 --- /dev/null +++ b/components/zcash_protocol/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "zcash_protocol" +description = "Zcash protocol network constants and value types." +version = "0.0.0" +authors = [ + "Jack Grigg ", + "Kris Nuttycombe ", +] +homepage = "https://github.com/zcash/librustzcash" +repository.workspace = true +readme = "README.md" +license.workspace = true +edition.workspace = true +rust-version.workspace = true +categories = ["cryptography::cryptocurrencies"] +keywords = ["zcash"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +# - Logging and metrics +memuse.workspace = true + +# Dependencies used internally: +# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.) +# - Documentation +document-features.workspace = true + +# - Test dependencies +proptest = { workspace = true, optional = true } +incrementalmerkletree = { workspace = true, optional = true } + +[dev-dependencies] +proptest.workspace = true + +[features] +## Exposes APIs that are useful for testing, such as `proptest` strategies. +test-dependencies = [ + "dep:incrementalmerkletree", + "dep:proptest", + "incrementalmerkletree?/test-dependencies", +] + +## Exposes support for working with a local consensus (e.g. regtest). +local-consensus = [] + +#! ### Experimental features +#! +#! ⚠️ Enabling these features will likely make your code incompatible with current Zcash +#! consensus rules! + +## Exposes the in-development NU6 features. +unstable-nu6 = [] + +## Exposes early in-development features that are not yet planned for any network upgrade. +zfuture = [] diff --git a/components/zcash_protocol/LICENSE-APACHE b/components/zcash_protocol/LICENSE-APACHE new file mode 100644 index 0000000000..1e5006dc14 --- /dev/null +++ b/components/zcash_protocol/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/components/zcash_protocol/LICENSE-MIT b/components/zcash_protocol/LICENSE-MIT new file mode 100644 index 0000000000..c869731ad4 --- /dev/null +++ b/components/zcash_protocol/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021-2024 Electric Coin Company + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/components/zcash_protocol/README.md b/components/zcash_protocol/README.md new file mode 100644 index 0000000000..862adf0a84 --- /dev/null +++ b/components/zcash_protocol/README.md @@ -0,0 +1,20 @@ +# zcash_protocol + +Zcash network constants and value types. + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/zcash_primitives/src/consensus.rs b/components/zcash_protocol/src/consensus.rs similarity index 81% rename from zcash_primitives/src/consensus.rs rename to components/zcash_protocol/src/consensus.rs index 8acd5c291b..dc4a5d775d 100644 --- a/zcash_primitives/src/consensus.rs +++ b/components/zcash_protocol/src/consensus.rs @@ -5,9 +5,8 @@ use std::cmp::{Ord, Ordering}; use std::convert::TryFrom; use std::fmt; use std::ops::{Add, Bound, RangeBounds, Sub}; -use zcash_address; -use crate::{constants, sapling::note_encryption::Zip212Enforcement}; +use crate::constants::{mainnet, regtest, testnet}; /// A wrapper type representing blockchain heights. /// @@ -136,68 +135,183 @@ impl Sub for BlockHeight { } } -/// Zcash consensus parameters. -pub trait Parameters: Clone { - /// Returns the activation height for a particular network upgrade, - /// if an activation height has been set. - fn activation_height(&self, nu: NetworkUpgrade) -> Option; - - /// Determines whether the specified network upgrade is active as of the - /// provided block height on the network to which this Parameters value applies. - fn is_nu_active(&self, nu: NetworkUpgrade, height: BlockHeight) -> bool { - self.activation_height(nu).map_or(false, |h| h <= height) - } - +/// Constants associated with a given Zcash network. +pub trait NetworkConstants: Clone { /// The coin type for ZEC, as defined by [SLIP 44]. /// /// [SLIP 44]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md fn coin_type(&self) -> u32; - /// Returns the standard network constant for address encoding. Returns - /// 'None' for nonstandard networks. - fn address_network(&self) -> Option; - /// Returns the human-readable prefix for Bech32-encoded Sapling extended spending keys - /// the network to which this Parameters value applies. + /// for the network to which this NetworkConstants value applies. /// /// Defined in [ZIP 32]. /// /// [`ExtendedSpendingKey`]: zcash_primitives::zip32::ExtendedSpendingKey /// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst - fn hrp_sapling_extended_spending_key(&self) -> &str; + fn hrp_sapling_extended_spending_key(&self) -> &'static str; /// Returns the human-readable prefix for Bech32-encoded Sapling extended full - /// viewing keys for the network to which this Parameters value applies. + /// viewing keys for the network to which this NetworkConstants value applies. /// /// Defined in [ZIP 32]. /// /// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey /// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst - fn hrp_sapling_extended_full_viewing_key(&self) -> &str; + fn hrp_sapling_extended_full_viewing_key(&self) -> &'static str; /// Returns the Bech32-encoded human-readable prefix for Sapling payment addresses - /// viewing keys for the network to which this Parameters value applies. + /// for the network to which this NetworkConstants value applies. /// /// Defined in section 5.6.4 of the [Zcash Protocol Specification]. /// /// [`PaymentAddress`]: zcash_primitives::primitives::PaymentAddress /// [Zcash Protocol Specification]: https://github.com/zcash/zips/blob/master/protocol/protocol.pdf - fn hrp_sapling_payment_address(&self) -> &str; + fn hrp_sapling_payment_address(&self) -> &'static str; + + /// Returns the human-readable prefix for Base58Check-encoded Sprout + /// payment addresses for the network to which this NetworkConstants value + /// applies. + /// + /// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. + /// + /// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding + fn b58_sprout_address_prefix(&self) -> [u8; 2]; /// Returns the human-readable prefix for Base58Check-encoded transparent - /// pay-to-public-key-hash payment addresses for the network to which this Parameters value + /// pay-to-public-key-hash payment addresses for the network to which this NetworkConstants value /// applies. /// /// [`TransparentAddress::PublicKey`]: zcash_primitives::legacy::TransparentAddress::PublicKey fn b58_pubkey_address_prefix(&self) -> [u8; 2]; /// Returns the human-readable prefix for Base58Check-encoded transparent pay-to-script-hash - /// payment addresses for the network to which this Parameters value applies. + /// payment addresses for the network to which this NetworkConstants value applies. /// /// [`TransparentAddress::Script`]: zcash_primitives::legacy::TransparentAddress::Script fn b58_script_address_prefix(&self) -> [u8; 2]; } +/// The enumeration of known Zcash network types. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum NetworkType { + /// Zcash Mainnet. + Main, + /// Zcash Testnet. + Test, + /// Private integration / regression testing, used in `zcashd`. + /// + /// For some address types there is no distinction between test and regtest encodings; + /// those will always be parsed as `Network::Test`. + Regtest, +} + +memuse::impl_no_dynamic_usage!(NetworkType); + +impl NetworkConstants for NetworkType { + fn coin_type(&self) -> u32 { + match self { + NetworkType::Main => mainnet::COIN_TYPE, + NetworkType::Test => testnet::COIN_TYPE, + NetworkType::Regtest => regtest::COIN_TYPE, + } + } + + fn hrp_sapling_extended_spending_key(&self) -> &'static str { + match self { + NetworkType::Main => mainnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, + NetworkType::Test => testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, + NetworkType::Regtest => regtest::HRP_SAPLING_EXTENDED_SPENDING_KEY, + } + } + + fn hrp_sapling_extended_full_viewing_key(&self) -> &'static str { + match self { + NetworkType::Main => mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + NetworkType::Test => testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + NetworkType::Regtest => regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, + } + } + + fn hrp_sapling_payment_address(&self) -> &'static str { + match self { + NetworkType::Main => mainnet::HRP_SAPLING_PAYMENT_ADDRESS, + NetworkType::Test => testnet::HRP_SAPLING_PAYMENT_ADDRESS, + NetworkType::Regtest => regtest::HRP_SAPLING_PAYMENT_ADDRESS, + } + } + + fn b58_sprout_address_prefix(&self) -> [u8; 2] { + match self { + NetworkType::Main => mainnet::B58_SPROUT_ADDRESS_PREFIX, + NetworkType::Test => testnet::B58_SPROUT_ADDRESS_PREFIX, + NetworkType::Regtest => regtest::B58_SPROUT_ADDRESS_PREFIX, + } + } + + fn b58_pubkey_address_prefix(&self) -> [u8; 2] { + match self { + NetworkType::Main => mainnet::B58_PUBKEY_ADDRESS_PREFIX, + NetworkType::Test => testnet::B58_PUBKEY_ADDRESS_PREFIX, + NetworkType::Regtest => regtest::B58_PUBKEY_ADDRESS_PREFIX, + } + } + + fn b58_script_address_prefix(&self) -> [u8; 2] { + match self { + NetworkType::Main => mainnet::B58_SCRIPT_ADDRESS_PREFIX, + NetworkType::Test => testnet::B58_SCRIPT_ADDRESS_PREFIX, + NetworkType::Regtest => regtest::B58_SCRIPT_ADDRESS_PREFIX, + } + } +} + +/// Zcash consensus parameters. +pub trait Parameters: Clone { + /// Returns the type of network configured by this set of consensus parameters. + fn network_type(&self) -> NetworkType; + + /// Returns the activation height for a particular network upgrade, + /// if an activation height has been set. + fn activation_height(&self, nu: NetworkUpgrade) -> Option; + + /// Determines whether the specified network upgrade is active as of the + /// provided block height on the network to which this Parameters value applies. + fn is_nu_active(&self, nu: NetworkUpgrade, height: BlockHeight) -> bool { + self.activation_height(nu).map_or(false, |h| h <= height) + } +} + +impl NetworkConstants for P { + fn coin_type(&self) -> u32 { + self.network_type().coin_type() + } + + fn hrp_sapling_extended_spending_key(&self) -> &'static str { + self.network_type().hrp_sapling_extended_spending_key() + } + + fn hrp_sapling_extended_full_viewing_key(&self) -> &'static str { + self.network_type().hrp_sapling_extended_full_viewing_key() + } + + fn hrp_sapling_payment_address(&self) -> &'static str { + self.network_type().hrp_sapling_payment_address() + } + + fn b58_sprout_address_prefix(&self) -> [u8; 2] { + self.network_type().b58_sprout_address_prefix() + } + + fn b58_pubkey_address_prefix(&self) -> [u8; 2] { + self.network_type().b58_pubkey_address_prefix() + } + + fn b58_script_address_prefix(&self) -> [u8; 2] { + self.network_type().b58_script_address_prefix() + } +} + /// Marker struct for the production network. #[derive(PartialEq, Eq, Copy, Clone, Debug)] pub struct MainNetwork; @@ -208,6 +322,10 @@ memuse::impl_no_dynamic_usage!(MainNetwork); pub const MAIN_NETWORK: MainNetwork = MainNetwork; impl Parameters for MainNetwork { + fn network_type(&self) -> NetworkType { + NetworkType::Main + } + fn activation_height(&self, nu: NetworkUpgrade) -> Option { match nu { NetworkUpgrade::Overwinter => Some(BlockHeight(347_500)), @@ -222,34 +340,6 @@ impl Parameters for MainNetwork { NetworkUpgrade::ZFuture => None, } } - - fn coin_type(&self) -> u32 { - constants::mainnet::COIN_TYPE - } - - fn address_network(&self) -> Option { - Some(zcash_address::Network::Main) - } - - fn hrp_sapling_extended_spending_key(&self) -> &str { - constants::mainnet::HRP_SAPLING_EXTENDED_SPENDING_KEY - } - - fn hrp_sapling_extended_full_viewing_key(&self) -> &str { - constants::mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY - } - - fn hrp_sapling_payment_address(&self) -> &str { - constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS - } - - fn b58_pubkey_address_prefix(&self) -> [u8; 2] { - constants::mainnet::B58_PUBKEY_ADDRESS_PREFIX - } - - fn b58_script_address_prefix(&self) -> [u8; 2] { - constants::mainnet::B58_SCRIPT_ADDRESS_PREFIX - } } /// Marker struct for the test network. @@ -262,6 +352,10 @@ memuse::impl_no_dynamic_usage!(TestNetwork); pub const TEST_NETWORK: TestNetwork = TestNetwork; impl Parameters for TestNetwork { + fn network_type(&self) -> NetworkType { + NetworkType::Test + } + fn activation_height(&self, nu: NetworkUpgrade) -> Option { match nu { NetworkUpgrade::Overwinter => Some(BlockHeight(207_500)), @@ -276,99 +370,31 @@ impl Parameters for TestNetwork { NetworkUpgrade::ZFuture => None, } } - - fn coin_type(&self) -> u32 { - constants::testnet::COIN_TYPE - } - - fn address_network(&self) -> Option { - Some(zcash_address::Network::Test) - } - - fn hrp_sapling_extended_spending_key(&self) -> &str { - constants::testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY - } - - fn hrp_sapling_extended_full_viewing_key(&self) -> &str { - constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY - } - - fn hrp_sapling_payment_address(&self) -> &str { - constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS - } - - fn b58_pubkey_address_prefix(&self) -> [u8; 2] { - constants::testnet::B58_PUBKEY_ADDRESS_PREFIX - } - - fn b58_script_address_prefix(&self) -> [u8; 2] { - constants::testnet::B58_SCRIPT_ADDRESS_PREFIX - } } -/// Marker enum for the deployed Zcash consensus networks. -#[derive(PartialEq, Eq, Copy, Clone, Debug)] +/// The enumeration of known Zcash networks. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Network { + /// Zcash Mainnet. MainNetwork, + /// Zcash Testnet. TestNetwork, } memuse::impl_no_dynamic_usage!(Network); impl Parameters for Network { - fn activation_height(&self, nu: NetworkUpgrade) -> Option { - match self { - Network::MainNetwork => MAIN_NETWORK.activation_height(nu), - Network::TestNetwork => TEST_NETWORK.activation_height(nu), - } - } - - fn coin_type(&self) -> u32 { + fn network_type(&self) -> NetworkType { match self { - Network::MainNetwork => MAIN_NETWORK.coin_type(), - Network::TestNetwork => TEST_NETWORK.coin_type(), + Network::MainNetwork => NetworkType::Main, + Network::TestNetwork => NetworkType::Test, } } - fn address_network(&self) -> Option { - match self { - Network::MainNetwork => Some(zcash_address::Network::Main), - Network::TestNetwork => Some(zcash_address::Network::Test), - } - } - - fn hrp_sapling_extended_spending_key(&self) -> &str { - match self { - Network::MainNetwork => MAIN_NETWORK.hrp_sapling_extended_spending_key(), - Network::TestNetwork => TEST_NETWORK.hrp_sapling_extended_spending_key(), - } - } - - fn hrp_sapling_extended_full_viewing_key(&self) -> &str { - match self { - Network::MainNetwork => MAIN_NETWORK.hrp_sapling_extended_full_viewing_key(), - Network::TestNetwork => TEST_NETWORK.hrp_sapling_extended_full_viewing_key(), - } - } - - fn hrp_sapling_payment_address(&self) -> &str { - match self { - Network::MainNetwork => MAIN_NETWORK.hrp_sapling_payment_address(), - Network::TestNetwork => TEST_NETWORK.hrp_sapling_payment_address(), - } - } - - fn b58_pubkey_address_prefix(&self) -> [u8; 2] { - match self { - Network::MainNetwork => MAIN_NETWORK.b58_pubkey_address_prefix(), - Network::TestNetwork => TEST_NETWORK.b58_pubkey_address_prefix(), - } - } - - fn b58_script_address_prefix(&self) -> [u8; 2] { + fn activation_height(&self, nu: NetworkUpgrade) -> Option { match self { - Network::MainNetwork => MAIN_NETWORK.b58_script_address_prefix(), - Network::TestNetwork => TEST_NETWORK.b58_script_address_prefix(), + Network::MainNetwork => MAIN_NETWORK.activation_height(nu), + Network::TestNetwork => TEST_NETWORK.activation_height(nu), } } } @@ -485,7 +511,7 @@ pub const ZIP212_GRACE_PERIOD: u32 = 32256; /// /// See [ZIP 200](https://zips.z.cash/zip-0200) for more details. /// -/// [`signature_hash`]: crate::transaction::sighash::signature_hash +/// [`signature_hash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/transaction/sighash/fn.signature_hash.html #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum BranchId { /// The consensus rules at the launch of Zcash. @@ -632,25 +658,6 @@ impl BranchId { } } -/// Returns the enforcement policy for ZIP 212 at the given height. -pub fn sapling_zip212_enforcement( - params: &impl Parameters, - height: BlockHeight, -) -> Zip212Enforcement { - if params.is_nu_active(NetworkUpgrade::Canopy, height) { - let grace_period_end_height = - params.activation_height(NetworkUpgrade::Canopy).unwrap() + ZIP212_GRACE_PERIOD; - - if height < grace_period_end_height { - Zip212Enforcement::GracePeriod - } else { - Zip212Enforcement::On - } - } else { - Zip212Enforcement::Off - } -} - #[cfg(any(test, feature = "test-dependencies"))] pub mod testing { use proptest::sample::select; @@ -688,6 +695,7 @@ pub mod testing { }) } + #[cfg(feature = "test-dependencies")] impl incrementalmerkletree::testing::TestCheckpoint for BlockHeight { fn from_u64(value: u64) -> Self { BlockHeight(u32::try_from(value).expect("Test checkpoint ids do not exceed 32 bits")) diff --git a/zcash_primitives/src/constants.rs b/components/zcash_protocol/src/constants.rs similarity index 100% rename from zcash_primitives/src/constants.rs rename to components/zcash_protocol/src/constants.rs diff --git a/zcash_primitives/src/constants/mainnet.rs b/components/zcash_protocol/src/constants/mainnet.rs similarity index 58% rename from zcash_primitives/src/constants/mainnet.rs rename to components/zcash_protocol/src/constants/mainnet.rs index 3b099e1389..25193fab88 100644 --- a/zcash_primitives/src/constants/mainnet.rs +++ b/components/zcash_protocol/src/constants/mainnet.rs @@ -9,7 +9,7 @@ pub const COIN_TYPE: u32 = 133; /// /// Defined in [ZIP 32]. /// -/// [`ExtendedSpendingKey`]: crate::sapling::zip32::ExtendedSpendingKey +/// [`ExtendedSpendingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedSpendingKey.html /// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-main"; @@ -17,7 +17,7 @@ pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-main"; /// /// Defined in [ZIP 32]. /// -/// [`ExtendedFullViewingKey`]: crate::sapling::zip32::ExtendedFullViewingKey +/// [`ExtendedFullViewingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedFullViewingKey.html /// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviews"; @@ -25,16 +25,23 @@ pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviews"; /// /// Defined in section 5.6.4 of the [Zcash Protocol Specification]. /// -/// [`PaymentAddress`]: crate::sapling::PaymentAddress +/// [`PaymentAddress`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/struct.PaymentAddress.html /// [Zcash Protocol Specification]: https://github.com/zcash/zips/blob/master/protocol/protocol.pdf pub const HRP_SAPLING_PAYMENT_ADDRESS: &str = "zs"; +/// The prefix for a Base58Check-encoded mainnet Sprout address. +/// +/// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. +/// +/// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding +pub const B58_SPROUT_ADDRESS_PREFIX: [u8; 2] = [0x16, 0x9a]; + /// The prefix for a Base58Check-encoded mainnet [`PublicKeyHash`]. /// -/// [`PublicKeyHash`]: crate::legacy::TransparentAddress::PublicKeyHash +/// [`PublicKeyHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xb8]; /// The prefix for a Base58Check-encoded mainnet [`ScriptHash`]. /// -/// [`ScriptHash`]: crate::legacy::TransparentAddress::ScriptHash +/// [`ScriptHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xbd]; diff --git a/zcash_primitives/src/constants/regtest.rs b/components/zcash_protocol/src/constants/regtest.rs similarity index 65% rename from zcash_primitives/src/constants/regtest.rs rename to components/zcash_protocol/src/constants/regtest.rs index c460ccbd41..2674a416de 100644 --- a/zcash_primitives/src/constants/regtest.rs +++ b/components/zcash_protocol/src/constants/regtest.rs @@ -13,7 +13,7 @@ pub const COIN_TYPE: u32 = 1; /// /// It is defined in [the `zcashd` codebase]. /// -/// [`ExtendedSpendingKey`]: crate::sapling::zip32::ExtendedSpendingKey +/// [`ExtendedSpendingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedSpendingKey.html /// [the `zcashd` codebase]: pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-regtest"; @@ -21,7 +21,7 @@ pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-regtest /// /// It is defined in [the `zcashd` codebase]. /// -/// [`ExtendedFullViewingKey`]: crate::sapling::zip32::ExtendedFullViewingKey +/// [`ExtendedFullViewingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedFullViewingKey.html /// [the `zcashd` codebase]: pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviewregtestsapling"; @@ -29,18 +29,26 @@ pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviewregtestsapling"; /// /// It is defined in [the `zcashd` codebase]. /// -/// [`PaymentAddress`]: crate::sapling::PaymentAddress +/// [`PaymentAddress`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/struct.PaymentAddress.html /// [the `zcashd` codebase]: pub const HRP_SAPLING_PAYMENT_ADDRESS: &str = "zregtestsapling"; +/// The prefix for a Base58Check-encoded regtest Sprout address. +/// +/// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. +/// Same as the testnet prefix. +/// +/// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding +pub const B58_SPROUT_ADDRESS_PREFIX: [u8; 2] = [0x16, 0xb6]; + /// The prefix for a Base58Check-encoded regtest transparent [`PublicKeyHash`]. /// Same as the testnet prefix. /// -/// [`PublicKeyHash`]: crate::legacy::TransparentAddress::PublicKeyHash +/// [`PublicKeyHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1d, 0x25]; /// The prefix for a Base58Check-encoded regtest transparent [`ScriptHash`]. /// Same as the testnet prefix. /// -/// [`ScriptHash`]: crate::legacy::TransparentAddress::ScriptHash +/// [`ScriptHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xba]; diff --git a/zcash_primitives/src/constants/testnet.rs b/components/zcash_protocol/src/constants/testnet.rs similarity index 59% rename from zcash_primitives/src/constants/testnet.rs rename to components/zcash_protocol/src/constants/testnet.rs index b61e188c36..bbe74b6a55 100644 --- a/zcash_primitives/src/constants/testnet.rs +++ b/components/zcash_protocol/src/constants/testnet.rs @@ -9,7 +9,7 @@ pub const COIN_TYPE: u32 = 1; /// /// Defined in [ZIP 32]. /// -/// [`ExtendedSpendingKey`]: crate::sapling::zip32::ExtendedSpendingKey +/// [`ExtendedSpendingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedSpendingKey.html /// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-test"; @@ -17,7 +17,7 @@ pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-test"; /// /// Defined in [ZIP 32]. /// -/// [`ExtendedFullViewingKey`]: crate::sapling::zip32::ExtendedFullViewingKey +/// [`ExtendedFullViewingKey`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/zip32/struct.ExtendedFullViewingKey.html /// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviewtestsapling"; @@ -25,16 +25,23 @@ pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviewtestsapling"; /// /// Defined in section 5.6.4 of the [Zcash Protocol Specification]. /// -/// [`PaymentAddress`]: crate::sapling::PaymentAddress +/// [`PaymentAddress`]: https://docs.rs/sapling-crypto/latest/sapling_crypto/struct.PaymentAddress.html /// [Zcash Protocol Specification]: https://github.com/zcash/zips/blob/master/protocol/protocol.pdf pub const HRP_SAPLING_PAYMENT_ADDRESS: &str = "ztestsapling"; +/// The prefix for a Base58Check-encoded testnet Sprout address. +/// +/// Defined in the [Zcash Protocol Specification section 5.6.3][sproutpaymentaddrencoding]. +/// +/// [sproutpaymentaddrencoding]: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding +pub const B58_SPROUT_ADDRESS_PREFIX: [u8; 2] = [0x16, 0xb6]; + /// The prefix for a Base58Check-encoded testnet transparent [`PublicKeyHash`]. /// -/// [`PublicKeyHash`]: crate::legacy::TransparentAddress::PublicKeyHash +/// [`PublicKeyHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html pub const B58_PUBKEY_ADDRESS_PREFIX: [u8; 2] = [0x1d, 0x25]; /// The prefix for a Base58Check-encoded testnet transparent [`ScriptHash`]. /// -/// [`ScriptHash`]: crate::legacy::TransparentAddress::ScriptHash +/// [`ScriptHash`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/legacy/enum.TransparentAddress.html pub const B58_SCRIPT_ADDRESS_PREFIX: [u8; 2] = [0x1c, 0xba]; diff --git a/components/zcash_protocol/src/lib.rs b/components/zcash_protocol/src/lib.rs new file mode 100644 index 0000000000..2976a02643 --- /dev/null +++ b/components/zcash_protocol/src/lib.rs @@ -0,0 +1,53 @@ +//! *A crate for Zcash protocol constants and value types.* +//! +//! `zcash_protocol` contains Rust structs, traits and functions that provide the network constants +//! for the Zcash main and test networks, as well types for representing ZEC amounts and value +//! balances. +//! +//! ## Feature flags +#![doc = document_features::document_features!()] +//! + +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +// Catch documentation errors caused by code changes. +#![deny(rustdoc::broken_intra_doc_links)] +// Temporary until we have addressed all Result cases. +#![allow(clippy::result_unit_err)] + +use core::fmt; + +pub mod consensus; +pub mod constants; +#[cfg(feature = "local-consensus")] +pub mod local_consensus; +pub mod memo; +pub mod value; + +/// A Zcash shielded transfer protocol. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum ShieldedProtocol { + /// The Sapling protocol + Sapling, + /// The Orchard protocol + Orchard, +} + +/// A value pool in the Zcash protocol. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum PoolType { + /// The transparent value pool + Transparent, + /// A shielded value pool. + Shielded(ShieldedProtocol), +} + +impl fmt::Display for PoolType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PoolType::Transparent => f.write_str("Transparent"), + PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"), + PoolType::Shielded(ShieldedProtocol::Orchard) => f.write_str("Orchard"), + } + } +} diff --git a/zcash_primitives/src/local_consensus.rs b/components/zcash_protocol/src/local_consensus.rs similarity index 79% rename from zcash_primitives/src/local_consensus.rs rename to components/zcash_protocol/src/local_consensus.rs index 8c4b647cc1..994154240c 100644 --- a/zcash_primitives/src/local_consensus.rs +++ b/components/zcash_protocol/src/local_consensus.rs @@ -1,7 +1,4 @@ -use crate::{ - consensus::{BlockHeight, NetworkUpgrade, Parameters}, - constants, -}; +use crate::consensus::{BlockHeight, NetworkType, NetworkUpgrade, Parameters}; /// a `LocalNetwork` setup should define the activation heights /// of network upgrades. `None` is considered as "not activated" @@ -36,7 +33,7 @@ use crate::{ /// }; /// ``` /// -#[derive(Clone, PartialEq, Eq, Copy, Debug)] +#[derive(Clone, PartialEq, Eq, Copy, Debug, Hash)] pub struct LocalNetwork { pub overwinter: Option, pub sapling: Option, @@ -44,18 +41,18 @@ pub struct LocalNetwork { pub heartwood: Option, pub canopy: Option, pub nu5: Option, + #[cfg(feature = "unstable-nu6")] pub nu6: Option, #[cfg(feature = "zfuture")] pub z_future: Option, } -/// Parameters default implementation for `LocalNetwork` -/// Important note: -/// The functions `coin_type()`, `address_network()`, -/// `hrp_sapling_extended_spending_key()`, `hrp_sapling_extended_full_viewing_key()`, -/// `hrp_sapling_payment_address()`, `b58_script_address_prefix()` return -/// `constants::regtest` values +/// Parameters implementation for `LocalNetwork` impl Parameters for LocalNetwork { + fn network_type(&self) -> NetworkType { + NetworkType::Regtest + } + fn activation_height(&self, nu: NetworkUpgrade) -> Option { match nu { NetworkUpgrade::Overwinter => self.overwinter, @@ -70,44 +67,12 @@ impl Parameters for LocalNetwork { NetworkUpgrade::ZFuture => self.z_future, } } - - fn coin_type(&self) -> u32 { - constants::regtest::COIN_TYPE - } - - fn address_network(&self) -> Option { - Some(zcash_address::Network::Regtest) - } - - fn hrp_sapling_extended_spending_key(&self) -> &str { - constants::regtest::HRP_SAPLING_EXTENDED_SPENDING_KEY - } - - fn hrp_sapling_extended_full_viewing_key(&self) -> &str { - constants::regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY - } - - fn hrp_sapling_payment_address(&self) -> &str { - constants::regtest::HRP_SAPLING_PAYMENT_ADDRESS - } - - fn b58_pubkey_address_prefix(&self) -> [u8; 2] { - constants::regtest::B58_PUBKEY_ADDRESS_PREFIX - } - - fn b58_script_address_prefix(&self) -> [u8; 2] { - constants::regtest::B58_SCRIPT_ADDRESS_PREFIX - } - - fn is_nu_active(&self, nu: NetworkUpgrade, height: BlockHeight) -> bool { - self.activation_height(nu).map_or(false, |h| h <= height) - } } #[cfg(test)] mod tests { use crate::{ - consensus::{BlockHeight, NetworkUpgrade, Parameters}, + consensus::{BlockHeight, NetworkConstants, NetworkUpgrade, Parameters}, constants, local_consensus::LocalNetwork, }; @@ -148,24 +113,6 @@ mod tests { assert!(regtest.is_nu_active(NetworkUpgrade::Nu6, expected_nu6)); #[cfg(feature = "zfuture")] assert!(!regtest.is_nu_active(NetworkUpgrade::ZFuture, expected_nu5)); - - assert_eq!(regtest.coin_type(), constants::regtest::COIN_TYPE); - assert_eq!( - regtest.hrp_sapling_extended_spending_key(), - constants::regtest::HRP_SAPLING_EXTENDED_SPENDING_KEY - ); - assert_eq!( - regtest.hrp_sapling_extended_full_viewing_key(), - constants::regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY - ); - assert_eq!( - regtest.hrp_sapling_payment_address(), - constants::regtest::HRP_SAPLING_PAYMENT_ADDRESS - ); - assert_eq!( - regtest.b58_pubkey_address_prefix(), - constants::regtest::B58_PUBKEY_ADDRESS_PREFIX - ); } #[test] diff --git a/zcash_primitives/src/memo.rs b/components/zcash_protocol/src/memo.rs similarity index 100% rename from zcash_primitives/src/memo.rs rename to components/zcash_protocol/src/memo.rs diff --git a/components/zcash_protocol/src/value.rs b/components/zcash_protocol/src/value.rs new file mode 100644 index 0000000000..395ac8edc2 --- /dev/null +++ b/components/zcash_protocol/src/value.rs @@ -0,0 +1,521 @@ +use std::convert::{Infallible, TryFrom}; +use std::error; +use std::iter::Sum; +use std::ops::{Add, Mul, Neg, Sub}; + +use memuse::DynamicUsage; + +pub const COIN: u64 = 1_0000_0000; +pub const MAX_MONEY: u64 = 21_000_000 * COIN; +pub const MAX_BALANCE: i64 = MAX_MONEY as i64; + +/// A type-safe representation of a Zcash value delta, in zatoshis. +/// +/// An ZatBalance can only be constructed from an integer that is within the valid monetary +/// range of `{-MAX_MONEY..MAX_MONEY}` (where `MAX_MONEY` = 21,000,000 × 10⁸ zatoshis), +/// and this is preserved as an invariant internally. (A [`Transaction`] containing serialized +/// invalid ZatBalances would also be rejected by the network consensus rules.) +/// +/// [`Transaction`]: https://docs.rs/zcash_primitives/latest/zcash_primitives/transaction/struct.Transaction.html +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)] +pub struct ZatBalance(i64); + +memuse::impl_no_dynamic_usage!(ZatBalance); + +impl ZatBalance { + /// Returns a zero-valued ZatBalance. + pub const fn zero() -> Self { + ZatBalance(0) + } + + /// Creates a constant ZatBalance from an i64. + /// + /// Panics: if the amount is outside the range `{-MAX_BALANCE..MAX_BALANCE}`. + pub const fn const_from_i64(amount: i64) -> Self { + assert!(-MAX_BALANCE <= amount && amount <= MAX_BALANCE); // contains is not const + ZatBalance(amount) + } + + /// Creates a constant ZatBalance from a u64. + /// + /// Panics: if the amount is outside the range `{0..MAX_BALANCE}`. + pub const fn const_from_u64(amount: u64) -> Self { + assert!(amount <= MAX_MONEY); // contains is not const + ZatBalance(amount as i64) + } + + /// Creates an ZatBalance from an i64. + /// + /// Returns an error if the amount is outside the range `{-MAX_BALANCE..MAX_BALANCE}`. + pub fn from_i64(amount: i64) -> Result { + if (-MAX_BALANCE..=MAX_BALANCE).contains(&amount) { + Ok(ZatBalance(amount)) + } else if amount < -MAX_BALANCE { + Err(BalanceError::Underflow) + } else { + Err(BalanceError::Overflow) + } + } + + /// Creates a non-negative ZatBalance from an i64. + /// + /// Returns an error if the amount is outside the range `{0..MAX_BALANCE}`. + pub fn from_nonnegative_i64(amount: i64) -> Result { + if (0..=MAX_BALANCE).contains(&amount) { + Ok(ZatBalance(amount)) + } else if amount < 0 { + Err(BalanceError::Underflow) + } else { + Err(BalanceError::Overflow) + } + } + + /// Creates an ZatBalance from a u64. + /// + /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. + pub fn from_u64(amount: u64) -> Result { + if amount <= MAX_MONEY { + Ok(ZatBalance(amount as i64)) + } else { + Err(BalanceError::Overflow) + } + } + + /// Reads an ZatBalance from a signed 64-bit little-endian integer. + /// + /// Returns an error if the amount is outside the range `{-MAX_BALANCE..MAX_BALANCE}`. + pub fn from_i64_le_bytes(bytes: [u8; 8]) -> Result { + let amount = i64::from_le_bytes(bytes); + ZatBalance::from_i64(amount) + } + + /// Reads a non-negative ZatBalance from a signed 64-bit little-endian integer. + /// + /// Returns an error if the amount is outside the range `{0..MAX_BALANCE}`. + pub fn from_nonnegative_i64_le_bytes(bytes: [u8; 8]) -> Result { + let amount = i64::from_le_bytes(bytes); + ZatBalance::from_nonnegative_i64(amount) + } + + /// Reads an ZatBalance from an unsigned 64-bit little-endian integer. + /// + /// Returns an error if the amount is outside the range `{0..MAX_BALANCE}`. + pub fn from_u64_le_bytes(bytes: [u8; 8]) -> Result { + let amount = u64::from_le_bytes(bytes); + ZatBalance::from_u64(amount) + } + + /// Returns the ZatBalance encoded as a signed 64-bit little-endian integer. + pub fn to_i64_le_bytes(self) -> [u8; 8] { + self.0.to_le_bytes() + } + + /// Returns `true` if `self` is positive and `false` if the ZatBalance is zero or + /// negative. + pub const fn is_positive(self) -> bool { + self.0.is_positive() + } + + /// Returns `true` if `self` is negative and `false` if the ZatBalance is zero or + /// positive. + pub const fn is_negative(self) -> bool { + self.0.is_negative() + } + + pub fn sum>(values: I) -> Option { + let mut result = ZatBalance::zero(); + for value in values { + result = (result + value)?; + } + Some(result) + } +} + +impl TryFrom for ZatBalance { + type Error = BalanceError; + + fn try_from(value: i64) -> Result { + ZatBalance::from_i64(value) + } +} + +impl From for i64 { + fn from(amount: ZatBalance) -> i64 { + amount.0 + } +} + +impl From<&ZatBalance> for i64 { + fn from(amount: &ZatBalance) -> i64 { + amount.0 + } +} + +impl TryFrom for u64 { + type Error = BalanceError; + + fn try_from(value: ZatBalance) -> Result { + value.0.try_into().map_err(|_| BalanceError::Underflow) + } +} + +impl Add for ZatBalance { + type Output = Option; + + fn add(self, rhs: ZatBalance) -> Option { + ZatBalance::from_i64(self.0 + rhs.0).ok() + } +} + +impl Add for Option { + type Output = Self; + + fn add(self, rhs: ZatBalance) -> Option { + self.and_then(|lhs| lhs + rhs) + } +} + +impl Sub for ZatBalance { + type Output = Option; + + fn sub(self, rhs: ZatBalance) -> Option { + ZatBalance::from_i64(self.0 - rhs.0).ok() + } +} + +impl Sub for Option { + type Output = Self; + + fn sub(self, rhs: ZatBalance) -> Option { + self.and_then(|lhs| lhs - rhs) + } +} + +impl Sum for Option { + fn sum>(iter: I) -> Self { + iter.fold(Some(ZatBalance::zero()), |acc, a| acc? + a) + } +} + +impl<'a> Sum<&'a ZatBalance> for Option { + fn sum>(iter: I) -> Self { + iter.fold(Some(ZatBalance::zero()), |acc, a| acc? + *a) + } +} + +impl Neg for ZatBalance { + type Output = Self; + + fn neg(self) -> Self { + ZatBalance(-self.0) + } +} + +impl Mul for ZatBalance { + type Output = Option; + + fn mul(self, rhs: usize) -> Option { + let rhs: i64 = rhs.try_into().ok()?; + self.0 + .checked_mul(rhs) + .and_then(|i| ZatBalance::try_from(i).ok()) + } +} + +/// A type-safe representation of some nonnegative amount of Zcash. +/// +/// A Zatoshis can only be constructed from an integer that is within the valid monetary +/// range of `{0..MAX_MONEY}` (where `MAX_MONEY` = 21,000,000 × 10⁸ zatoshis). +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)] +pub struct Zatoshis(u64); + +impl Zatoshis { + /// Returns the identity `Zatoshis` + pub const ZERO: Self = Zatoshis(0); + + /// Returns this Zatoshis as a u64. + pub fn into_u64(self) -> u64 { + self.0 + } + + /// Creates a Zatoshis from a u64. + /// + /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. + pub fn from_u64(amount: u64) -> Result { + if (0..=MAX_MONEY).contains(&amount) { + Ok(Zatoshis(amount)) + } else { + Err(BalanceError::Overflow) + } + } + + /// Creates a constant Zatoshis from a u64. + /// + /// Panics: if the amount is outside the range `{0..MAX_MONEY}`. + pub const fn const_from_u64(amount: u64) -> Self { + assert!(amount <= MAX_MONEY); // contains is not const + Zatoshis(amount) + } + + /// Creates a Zatoshis from an i64. + /// + /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. + pub fn from_nonnegative_i64(amount: i64) -> Result { + u64::try_from(amount) + .map_err(|_| BalanceError::Underflow) + .and_then(Self::from_u64) + } + + /// Reads an Zatoshis from an unsigned 64-bit little-endian integer. + /// + /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. + pub fn from_u64_le_bytes(bytes: [u8; 8]) -> Result { + let amount = u64::from_le_bytes(bytes); + Self::from_u64(amount) + } + + /// Reads a Zatoshis from a signed integer represented as a two's + /// complement 64-bit little-endian value. + /// + /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. + pub fn from_nonnegative_i64_le_bytes(bytes: [u8; 8]) -> Result { + let amount = i64::from_le_bytes(bytes); + Self::from_nonnegative_i64(amount) + } + + /// Returns this Zatoshis encoded as a signed two's complement 64-bit + /// little-endian value. + pub fn to_i64_le_bytes(self) -> [u8; 8] { + (self.0 as i64).to_le_bytes() + } + + /// Returns whether or not this `Zatoshis` is the zero value. + pub fn is_zero(&self) -> bool { + self == &Zatoshis::ZERO + } + + /// Returns whether or not this `Zatoshis` is positive. + pub fn is_positive(&self) -> bool { + self > &Zatoshis::ZERO + } +} + +impl From for ZatBalance { + fn from(n: Zatoshis) -> Self { + ZatBalance(n.0 as i64) + } +} + +impl From<&Zatoshis> for ZatBalance { + fn from(n: &Zatoshis) -> Self { + ZatBalance(n.0 as i64) + } +} + +impl From for u64 { + fn from(n: Zatoshis) -> Self { + n.into_u64() + } +} + +impl TryFrom for Zatoshis { + type Error = BalanceError; + + fn try_from(value: u64) -> Result { + Zatoshis::from_u64(value) + } +} + +impl TryFrom for Zatoshis { + type Error = BalanceError; + + fn try_from(value: ZatBalance) -> Result { + Zatoshis::from_nonnegative_i64(value.0) + } +} + +impl Add for Zatoshis { + type Output = Option; + + fn add(self, rhs: Zatoshis) -> Option { + Self::from_u64(self.0.checked_add(rhs.0)?).ok() + } +} + +impl Add for Option { + type Output = Self; + + fn add(self, rhs: Zatoshis) -> Option { + self.and_then(|lhs| lhs + rhs) + } +} + +impl Sub for Zatoshis { + type Output = Option; + + fn sub(self, rhs: Zatoshis) -> Option { + Zatoshis::from_u64(self.0.checked_sub(rhs.0)?).ok() + } +} + +impl Sub for Option { + type Output = Self; + + fn sub(self, rhs: Zatoshis) -> Option { + self.and_then(|lhs| lhs - rhs) + } +} + +impl Mul for Zatoshis { + type Output = Option; + + fn mul(self, rhs: usize) -> Option { + Zatoshis::from_u64(self.0.checked_mul(u64::try_from(rhs).ok()?)?).ok() + } +} + +impl Sum for Option { + fn sum>(iter: I) -> Self { + iter.fold(Some(Zatoshis::ZERO), |acc, a| acc? + a) + } +} + +impl<'a> Sum<&'a Zatoshis> for Option { + fn sum>(iter: I) -> Self { + iter.fold(Some(Zatoshis::ZERO), |acc, a| acc? + *a) + } +} + +/// A type for balance violations in amount addition and subtraction +/// (overflow and underflow of allowed ranges) +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum BalanceError { + Overflow, + Underflow, +} + +impl error::Error for BalanceError {} + +impl std::fmt::Display for BalanceError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match &self { + BalanceError::Overflow => { + write!( + f, + "ZatBalance addition resulted in a value outside the valid range." + ) + } + BalanceError::Underflow => write!( + f, + "ZatBalance subtraction resulted in a value outside the valid range." + ), + } + } +} + +impl From for BalanceError { + fn from(_value: Infallible) -> Self { + unreachable!() + } +} + +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use proptest::prelude::prop_compose; + + use super::{ZatBalance, Zatoshis, MAX_BALANCE, MAX_MONEY}; + + prop_compose! { + pub fn arb_zat_balance()(amt in -MAX_BALANCE..MAX_BALANCE) -> ZatBalance { + ZatBalance::from_i64(amt).unwrap() + } + } + + prop_compose! { + pub fn arb_positive_zat_balance()(amt in 1i64..MAX_BALANCE) -> ZatBalance { + ZatBalance::from_i64(amt).unwrap() + } + } + + prop_compose! { + pub fn arb_nonnegative_zat_balance()(amt in 0i64..MAX_BALANCE) -> ZatBalance { + ZatBalance::from_i64(amt).unwrap() + } + } + + prop_compose! { + pub fn arb_zatoshis()(amt in 0u64..MAX_MONEY) -> Zatoshis { + Zatoshis::from_u64(amt).unwrap() + } + } +} + +#[cfg(test)] +mod tests { + use crate::value::MAX_BALANCE; + + use super::ZatBalance; + + #[test] + fn amount_in_range() { + let zero = b"\x00\x00\x00\x00\x00\x00\x00\x00"; + assert_eq!(ZatBalance::from_u64_le_bytes(*zero).unwrap(), ZatBalance(0)); + assert_eq!( + ZatBalance::from_nonnegative_i64_le_bytes(*zero).unwrap(), + ZatBalance(0) + ); + assert_eq!(ZatBalance::from_i64_le_bytes(*zero).unwrap(), ZatBalance(0)); + + let neg_one = b"\xff\xff\xff\xff\xff\xff\xff\xff"; + assert!(ZatBalance::from_u64_le_bytes(*neg_one).is_err()); + assert!(ZatBalance::from_nonnegative_i64_le_bytes(*neg_one).is_err()); + assert_eq!( + ZatBalance::from_i64_le_bytes(*neg_one).unwrap(), + ZatBalance(-1) + ); + + let max_money = b"\x00\x40\x07\x5a\xf0\x75\x07\x00"; + assert_eq!( + ZatBalance::from_u64_le_bytes(*max_money).unwrap(), + ZatBalance(MAX_BALANCE) + ); + assert_eq!( + ZatBalance::from_nonnegative_i64_le_bytes(*max_money).unwrap(), + ZatBalance(MAX_BALANCE) + ); + assert_eq!( + ZatBalance::from_i64_le_bytes(*max_money).unwrap(), + ZatBalance(MAX_BALANCE) + ); + + let max_money_p1 = b"\x01\x40\x07\x5a\xf0\x75\x07\x00"; + assert!(ZatBalance::from_u64_le_bytes(*max_money_p1).is_err()); + assert!(ZatBalance::from_nonnegative_i64_le_bytes(*max_money_p1).is_err()); + assert!(ZatBalance::from_i64_le_bytes(*max_money_p1).is_err()); + + let neg_max_money = b"\x00\xc0\xf8\xa5\x0f\x8a\xf8\xff"; + assert!(ZatBalance::from_u64_le_bytes(*neg_max_money).is_err()); + assert!(ZatBalance::from_nonnegative_i64_le_bytes(*neg_max_money).is_err()); + assert_eq!( + ZatBalance::from_i64_le_bytes(*neg_max_money).unwrap(), + ZatBalance(-MAX_BALANCE) + ); + + let neg_max_money_m1 = b"\xff\xbf\xf8\xa5\x0f\x8a\xf8\xff"; + assert!(ZatBalance::from_u64_le_bytes(*neg_max_money_m1).is_err()); + assert!(ZatBalance::from_nonnegative_i64_le_bytes(*neg_max_money_m1).is_err()); + assert!(ZatBalance::from_i64_le_bytes(*neg_max_money_m1).is_err()); + } + + #[test] + fn add_overflow() { + let v = ZatBalance(MAX_BALANCE); + assert_eq!(v + ZatBalance(1), None) + } + + #[test] + fn sub_underflow() { + let v = ZatBalance(-MAX_BALANCE); + assert_eq!(v - ZatBalance(1), None) + } +} diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index e6e34ffc45..87b13bab85 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -53,6 +53,21 @@ and this library adheres to Rust's notion of - Added method `WalletRead::validate_seed` - `zcash_client_backend::fees`: - Arguments to `ChangeStrategy::compute_balance` have changed. +- `zcash_client_backend::zip321::render::amount_str` now takes a + `NonNegativeAmount` rather than a signed `Amount` as its argument. +- `zcash_client_backend::zip321::parse::parse_amount` now parses a + `NonNegativeAmount` rather than a signed `Amount`. +- `zcash_client_backend::zip321::TransactionRequest::total` now + returns `Result<_, BalanceError>` instead of `Result<_, ()>`. + +### Removed +- `zcash_client_backend::PoolType::is_receiver`: use + `zcash_keys::Address::has_receiver` instead. + +### Fixed +- This release fixes an error in amount parsing in `zip321` that previously + allowed amounts having a decimal point but no decimal value to be parsed + as valid. ## [0.11.0] - 2024-03-01 diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index 43605960a5..a9dce2f686 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -30,6 +30,7 @@ zcash_encoding.workspace = true zcash_keys = { workspace = true, features = ["sapling"] } zcash_note_encryption.workspace = true zcash_primitives.workspace = true +zcash_protocol.workspace = true zip32.workspace = true # Dependencies exposed in a public API: diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index 6648810fd8..dd3014fa98 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -41,7 +41,10 @@ use zcash_primitives::{ memo::MemoBytes, transaction::{ builder::{BuildConfig, BuildResult, Builder}, - components::amount::{Amount, NonNegativeAmount}, + components::{ + amount::{Amount, NonNegativeAmount}, + sapling::zip212_enforcement, + }, fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule}, Transaction, TxId, }, @@ -1138,10 +1141,7 @@ where try_sapling_note_decryption( &sapling_internal_ivk, &bundle.shielded_outputs()[output_index], - consensus::sapling_zip212_enforcement( - params, - min_target_height, - ), + zip212_enforcement(params, min_target_height), ) .map(|(note, _, _)| Note::Sapling(note)) }) diff --git a/zcash_client_backend/src/decrypt.rs b/zcash_client_backend/src/decrypt.rs index e658b0658a..91a8e2a8ab 100644 --- a/zcash_client_backend/src/decrypt.rs +++ b/zcash_client_backend/src/decrypt.rs @@ -6,6 +6,7 @@ use sapling::note_encryption::{ use zcash_primitives::{ consensus::{self, BlockHeight}, memo::MemoBytes, + transaction::components::sapling::zip212_enforcement, transaction::Transaction, zip32::Scope, }; @@ -53,7 +54,7 @@ pub fn decrypt_transaction( tx: &Transaction, ufvks: &HashMap, ) -> Vec> { - let zip212_enforcement = consensus::sapling_zip212_enforcement(params, height); + let zip212_enforcement = zip212_enforcement(params, height); tx.sapling_bundle() .iter() .flat_map(|bundle| { diff --git a/zcash_client_backend/src/fees/sapling.rs b/zcash_client_backend/src/fees/sapling.rs index 2653310a92..fa7ef6157e 100644 --- a/zcash_client_backend/src/fees/sapling.rs +++ b/zcash_client_backend/src/fees/sapling.rs @@ -67,7 +67,7 @@ impl InputView<()> for SpendInfo { } fn value(&self) -> NonNegativeAmount { - NonNegativeAmount::try_from(self.value()) + NonNegativeAmount::try_from(self.value().inner()) .expect("An existing note to be spent must have a valid amount value.") } } @@ -81,7 +81,7 @@ pub trait OutputView { impl OutputView for OutputInfo { fn value(&self) -> NonNegativeAmount { - NonNegativeAmount::try_from(self.value()) + NonNegativeAmount::try_from(self.value().inner()) .expect("Output values should be checked at construction.") } } diff --git a/zcash_client_backend/src/lib.rs b/zcash_client_backend/src/lib.rs index a4d88cee7b..7c5d32cef1 100644 --- a/zcash_client_backend/src/lib.rs +++ b/zcash_client_backend/src/lib.rs @@ -64,7 +64,6 @@ pub use zcash_keys::address; pub mod data_api; mod decrypt; -use zcash_keys::address::Address; pub use zcash_keys::encoding; pub mod fees; pub use zcash_keys::keys; @@ -78,9 +77,8 @@ pub mod zip321; #[cfg(feature = "unstable-serialization")] pub mod serialization; -use std::fmt; - pub use decrypt::{decrypt_transaction, DecryptedOutput, TransferType}; +pub use zcash_protocol::{PoolType, ShieldedProtocol}; #[cfg(test)] #[macro_use] @@ -90,51 +88,3 @@ extern crate assert_matches; core::compile_error!( "The `orchard` feature flag requires the `zcash_unstable=\"orchard\"` RUSTFLAG." ); - -/// A shielded transfer protocol known to the wallet. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum ShieldedProtocol { - /// The Sapling protocol - Sapling, - /// The Orchard protocol - Orchard, -} - -/// A value pool to which the wallet supports sending transaction outputs. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum PoolType { - /// The transparent value pool - Transparent, - /// A shielded value pool. - Shielded(ShieldedProtocol), -} - -impl PoolType { - pub fn is_receiver(&self, addr: &Address) -> bool { - match addr { - Address::Sapling(_) => matches!(self, PoolType::Shielded(ShieldedProtocol::Sapling)), - Address::Transparent(_) => matches!(self, PoolType::Transparent), - Address::Unified(ua) => match self { - PoolType::Transparent => ua.transparent().is_some(), - PoolType::Shielded(ShieldedProtocol::Sapling) => ua.sapling().is_some(), - PoolType::Shielded(ShieldedProtocol::Orchard) => { - #[cfg(feature = "orchard")] - return ua.orchard().is_some(); - - #[cfg(not(feature = "orchard"))] - return false; - } - }, - } - } -} - -impl fmt::Display for PoolType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - PoolType::Transparent => f.write_str("Transparent"), - PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"), - PoolType::Shielded(ShieldedProtocol::Orchard) => f.write_str("Orchard"), - } - } -} diff --git a/zcash_client_backend/src/proposal.rs b/zcash_client_backend/src/proposal.rs index 759aaa578a..cb5e004f86 100644 --- a/zcash_client_backend/src/proposal.rs +++ b/zcash_client_backend/src/proposal.rs @@ -377,7 +377,7 @@ impl Step { .payments() .get(idx) .iter() - .any(|payment| pool.is_receiver(&payment.recipient_address)) + .any(|payment| payment.recipient_address.has_receiver(*pool)) { return Err(ProposalError::PaymentPoolsMismatch); } diff --git a/zcash_client_backend/src/scanning.rs b/zcash_client_backend/src/scanning.rs index 8f132055ea..4a99f87547 100644 --- a/zcash_client_backend/src/scanning.rs +++ b/zcash_client_backend/src/scanning.rs @@ -11,11 +11,12 @@ use sapling::{ SaplingIvk, }; use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption}; + use zcash_keys::keys::UnifiedFullViewingKey; use zcash_note_encryption::{batch, BatchDomain, Domain, ShieldedOutput, COMPACT_NOTE_SIZE}; use zcash_primitives::{ consensus::{self, BlockHeight, NetworkUpgrade}, - transaction::TxId, + transaction::{components::sapling::zip212_enforcement, TxId}, }; use zip32::Scope; @@ -594,7 +595,7 @@ where { let block_hash = block.hash(); let block_height = block.height(); - let zip212_enforcement = consensus::sapling_zip212_enforcement(params, block_height); + let zip212_enforcement = zip212_enforcement(params, block_height); for tx in block.vtx.into_iter() { let txid = tx.txid(); @@ -687,7 +688,7 @@ where let cur_height = block.height(); let cur_hash = block.hash(); - let zip212_enforcement = consensus::sapling_zip212_enforcement(params, cur_height); + let zip212_enforcement = zip212_enforcement(params, cur_height); let mut sapling_commitment_tree_size = prior_block_metadata .and_then(|m| m.sapling_tree_size()) @@ -1147,9 +1148,9 @@ mod tests { use zcash_note_encryption::{Domain, COMPACT_NOTE_SIZE}; use zcash_primitives::{ block::BlockHash, - consensus::{sapling_zip212_enforcement, BlockHeight, Network}, + consensus::{BlockHeight, Network}, memo::MemoBytes, - transaction::components::amount::NonNegativeAmount, + transaction::components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, zip32::AccountId, }; @@ -1210,13 +1211,13 @@ mod tests { tx_after: bool, initial_tree_sizes: Option<(u32, u32)>, ) -> CompactBlock { - let zip212_enforcement = sapling_zip212_enforcement(&Network::TestNetwork, height); + let zip212_enforcement = zip212_enforcement(&Network::TestNetwork, height); let to = dfvk.default_address().1; // Create a fake Note for the account let mut rng = OsRng; let rseed = generate_random_rseed(zip212_enforcement, &mut rng); - let note = sapling::Note::from_parts(to, NoteValue::from(value), rseed); + let note = sapling::Note::from_parts(to, NoteValue::from_raw(value.into()), rseed); let encryptor = sapling_note_encryption( Some(dfvk.fvk().ovk), note.clone(), diff --git a/zcash_client_backend/src/wallet.rs b/zcash_client_backend/src/wallet.rs index b91ebfad0f..c65103a2e2 100644 --- a/zcash_client_backend/src/wallet.rs +++ b/zcash_client_backend/src/wallet.rs @@ -361,7 +361,7 @@ pub enum Note { impl Note { pub fn value(&self) -> NonNegativeAmount { match self { - Note::Sapling(n) => n.value().try_into().expect( + Note::Sapling(n) => n.value().inner().try_into().expect( "Sapling notes must have values in the range of valid non-negative ZEC values.", ), #[cfg(feature = "orchard")] @@ -461,6 +461,7 @@ impl sapling_fees::InputView for ReceivedNote NonNegativeAmount { self.note .value() + .inner() .try_into() .expect("Sapling note values are indirectly checked by consensus.") } @@ -475,6 +476,7 @@ impl orchard_fees::InputView for ReceivedNote NonNegativeAmount { self.note .value() + .inner() .try_into() .expect("Orchard note values are indirectly checked by consensus.") } diff --git a/zcash_client_backend/src/zip321.rs b/zcash_client_backend/src/zip321.rs index 9292206d06..bd020bb3e1 100644 --- a/zcash_client_backend/src/zip321.rs +++ b/zcash_client_backend/src/zip321.rs @@ -16,10 +16,10 @@ use nom::{ sequence::preceded, }; use zcash_primitives::{ - consensus, memo::{self, MemoBytes}, transaction::components::amount::NonNegativeAmount, }; +use zcash_protocol::{consensus, value::BalanceError}; use crate::address::Address; @@ -206,11 +206,13 @@ impl TransactionRequest { /// /// Returns `Err` in the case of overflow, or if the value is /// outside the range `0..=MAX_MONEY` zatoshis. - pub fn total(&self) -> Result { + pub fn total(&self) -> Result { self.payments .values() .map(|p| p.amount) - .fold(Ok(NonNegativeAmount::ZERO), |acc, a| (acc? + a).ok_or(())) + .fold(Ok(NonNegativeAmount::ZERO), |acc, a| { + (acc? + a).ok_or(BalanceError::Overflow) + }) } /// A utility for use in tests to help check round-trip serialization properties. @@ -243,7 +245,7 @@ impl TransactionRequest { payment_index: Option, ) -> impl IntoIterator + '_ { std::iter::empty() - .chain(render::amount_param(payment.amount, payment_index)) + .chain(Some(render::amount_param(payment.amount, payment_index))) .chain( payment .memo @@ -306,8 +308,8 @@ impl TransactionRequest { /// Parse the provided URI to a payment request value. pub fn from_uri(params: &P, uri: &str) -> Result { // Parse the leading zcash:
- let (rest, primary_addr_param) = - parse::lead_addr(params)(uri).map_err(|e| Zip321Error::ParseError(e.to_string()))?; + let (rest, primary_addr_param) = parse::lead_addr(params)(uri) + .map_err(|e| Zip321Error::ParseError(format!("Error parsing lead address: {}", e)))?; // Parse the remaining parameters as an undifferentiated list let (_, xs) = if rest.is_empty() { @@ -317,7 +319,9 @@ impl TransactionRequest { char('?'), separated_list0(char('&'), parse::zcashparam(params)), ))(rest) - .map_err(|e| Zip321Error::ParseError(e.to_string()))? + .map_err(|e| { + Zip321Error::ParseError(format!("Error parsing query parameters: {}", e)) + })? }; // Construct sets of payment parameters, keyed by the payment index. @@ -358,9 +362,8 @@ mod render { use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; use zcash_primitives::{ - consensus, + consensus, transaction::components::amount::NonNegativeAmount, transaction::components::amount::COIN, - transaction::components::{amount::NonNegativeAmount, Amount}, }; use super::{memo_to_base64, Address, MemoBytes}; @@ -411,28 +414,24 @@ mod render { format!("address{}={}", param_index(idx), addr.encode(params)) } - /// Converts an [`Amount`] value to a correctly formatted decimal ZEC - /// value for inclusion in a ZIP 321 URI. - pub fn amount_str(amount: Amount) -> Option { - if amount.is_positive() { - let coins = i64::from(amount) / COIN; - let zats = i64::from(amount) % COIN; - Some(if zats == 0 { - format!("{}", coins) - } else { - format!("{}.{:0>8}", coins, zats) - .trim_end_matches('0') - .to_string() - }) + /// Converts a [`NonNegativeAmount`] value to a correctly formatted decimal ZEC + /// string for inclusion in a ZIP 321 URI. + pub fn amount_str(amount: NonNegativeAmount) -> String { + let coins = u64::from(amount) / COIN; + let zats = u64::from(amount) % COIN; + if zats == 0 { + format!("{}", coins) } else { - None + format!("{}.{:0>8}", coins, zats) + .trim_end_matches('0') + .to_string() } } /// Constructs an "amount" key/value pair containing the encoded ZEC amount /// at the specified parameter index. - pub fn amount_param(amount: NonNegativeAmount, idx: Option) -> Option { - amount_str(amount.into()).map(|s| format!("amount{}={}", param_index(idx), s)) + pub fn amount_param(amount: NonNegativeAmount, idx: Option) -> String { + format!("amount{}={}", param_index(idx), amount_str(amount)) } /// Constructs a "memo" key/value pair containing the base64URI-encoded memo @@ -459,16 +458,16 @@ mod parse { use nom::{ bytes::complete::{tag, take_till}, character::complete::{alpha1, char, digit0, digit1, one_of}, - combinator::{map_opt, map_res, opt, recognize}, + combinator::{all_consuming, map_opt, map_res, opt, recognize}, sequence::{preceded, separated_pair, tuple}, AsChar, IResult, InputTakeAtPosition, }; use percent_encoding::percent_decode; use zcash_primitives::{ - consensus, + consensus, transaction::components::amount::NonNegativeAmount, transaction::components::amount::COIN, - transaction::components::{amount::NonNegativeAmount, Amount}, }; + use zcash_protocol::value::BalanceError; use crate::address::Address; @@ -641,39 +640,34 @@ mod parse { } /// Parses a value in decimal ZEC. - pub fn parse_amount(input: &str) -> IResult<&str, Amount> { + pub fn parse_amount(input: &str) -> IResult<&str, NonNegativeAmount> { map_res( - tuple(( + all_consuming(tuple(( digit1, opt(preceded( char('.'), - map_opt(digit0, |s: &str| if s.len() > 8 { None } else { Some(s) }), + map_opt(digit1, |s: &str| if s.len() > 8 { None } else { Some(s) }), )), - )), + ))), |(whole_s, decimal_s): (&str, Option<&str>)| { - let coins: i64 = whole_s + let coins: u64 = whole_s .to_string() - .parse::() + .parse::() .map_err(|e| e.to_string())?; - let zats: i64 = match decimal_s { + let zats: u64 = match decimal_s { Some(d) => format!("{:0<8}", d) - .parse::() + .parse::() .map_err(|e| e.to_string())?, None => 0, }; - if coins >= 21000000 && (coins > 21000000 || zats > 0) { - return Err(format!( - "{} coins exceeds the maximum possible Zcash value.", - coins - )); - } - - let amt = coins * COIN + zats; - - Amount::from_nonnegative_i64(amt) - .map_err(|_| format!("Not a valid zat amount: {}", amt)) + coins + .checked_mul(COIN) + .and_then(|coin_zats| coin_zats.checked_add(zats)) + .ok_or(BalanceError::Overflow) + .and_then(NonNegativeAmount::from_u64) + .map_err(|_| format!("Not a valid amount: {} ZEC", input)) }, )(input) } @@ -693,11 +687,7 @@ mod parse { "amount" => parse_amount(value) .map_err(|e| e.to_string()) - .and_then(|(_, a)| { - NonNegativeAmount::try_from(a) - .map_err(|_| "Payment amount must be nonnegative.".to_owned()) - }) - .map(Param::Amount), + .map(|(_, amt)| Param::Amount(amt)), "label" => percent_decode(value.as_bytes()) .decode_utf8() @@ -830,12 +820,10 @@ mod tests { use zcash_keys::address::testing::arb_addr; use zcash_primitives::{ - consensus::{Parameters, TEST_NETWORK}, memo::Memo, - transaction::components::amount::{ - testing::arb_nonnegative_amount, Amount, NonNegativeAmount, - }, + transaction::components::amount::{testing::arb_nonnegative_amount, NonNegativeAmount}, }; + use zcash_protocol::consensus::{NetworkConstants, NetworkType, TEST_NETWORK}; #[cfg(feature = "local-consensus")] use zcash_primitives::{local_consensus::LocalNetwork, BlockHeight}; @@ -861,8 +849,8 @@ mod tests { let amounts = vec![1u64, 1000u64, 100000u64, 100000000u64, 100000000000u64]; for amt_u64 in amounts { - let amt = Amount::from_u64(amt_u64).unwrap(); - let amt_str = amount_str(amt).unwrap(); + let amt = NonNegativeAmount::from_u64(amt_u64).unwrap(); + let amt_str = amount_str(amt); assert_eq!(amt, parse_amount(&amt_str).unwrap().1); } } @@ -883,7 +871,7 @@ mod tests { let expected = TransactionRequest::new( vec![ Payment { - recipient_address: Address::Sapling(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()), + recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()), amount: NonNegativeAmount::const_from_u64(376876902796286), memo: None, label: None, @@ -904,7 +892,7 @@ mod tests { let expected = TransactionRequest::new( vec![ Payment { - recipient_address: Address::Sapling(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()), + recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()), amount: NonNegativeAmount::ZERO, memo: None, label: None, @@ -922,7 +910,7 @@ mod tests { let req = TransactionRequest::new( vec![ Payment { - recipient_address: Address::Sapling(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()), + recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()), amount: NonNegativeAmount::ZERO, memo: None, label: None, @@ -1090,6 +1078,11 @@ mod tests { "zcash:?amount.10000=1.23&address.10000=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU"; let i10r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_10); assert!(i10r.is_err()); + + // invalid: bad amount format + let invalid_11 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123."; + let i11r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_11); + assert!(i11r.is_err()); } proptest! { @@ -1106,9 +1099,8 @@ mod tests { } #[test] - fn prop_zip321_roundtrip_amount(nn_amt in arb_nonnegative_amount()) { - let amt = Amount::from(nn_amt); - let amt_str = amount_str(amt).unwrap(); + fn prop_zip321_roundtrip_amount(amt in arb_nonnegative_amount()) { + let amt_str = amount_str(amt); assert_eq!(amt, parse_amount(&amt_str).unwrap().1); } diff --git a/zcash_client_sqlite/CHANGELOG.md b/zcash_client_sqlite/CHANGELOG.md index f8d353446e..fbf30976e6 100644 --- a/zcash_client_sqlite/CHANGELOG.md +++ b/zcash_client_sqlite/CHANGELOG.md @@ -10,6 +10,14 @@ and this library adheres to Rust's notion of ### Added - A new `orchard` feature flag has been added to make it possible to build client code without `orchard` dependendencies. +- `impl From for SqliteClientError` + +### Changed +- `zcash_client_sqlite::error::SqliteClientError` has changed variants: + - Added `AddressGeneration` + - Removed `DiversifierIndexOutOfRange` +- `zcash_client_sqlite::wallet::init::WalletMigrationError` has added variant + `AddressGeneration` ## [0.9.0] - 2024-03-01 diff --git a/zcash_client_sqlite/src/error.rs b/zcash_client_sqlite/src/error.rs index c1993c39b8..7ec27b5d4d 100644 --- a/zcash_client_sqlite/src/error.rs +++ b/zcash_client_sqlite/src/error.rs @@ -8,6 +8,7 @@ use zcash_client_backend::{ encoding::{Bech32DecodeError, TransparentCodecError}, PoolType, }; +use zcash_keys::keys::AddressGenerationError; use zcash_primitives::{ consensus::BlockHeight, transaction::components::amount::BalanceError, zip32::AccountId, }; @@ -68,8 +69,8 @@ pub enum SqliteClientError { /// this error is (safe rewind height, requested height). RequestedRewindInvalid(BlockHeight, BlockHeight), - /// The space of allocatable diversifier indices has been exhausted for the given account. - DiversifierIndexOutOfRange, + /// An error occurred in generating a Zcash address. + AddressGeneration(AddressGenerationError), /// The account for which information was requested does not belong to the wallet. AccountUnknown(AccountId), @@ -119,6 +120,7 @@ impl error::Error for SqliteClientError { SqliteClientError::DbError(e) => Some(e), SqliteClientError::Io(e) => Some(e), SqliteClientError::BalanceError(e) => Some(e), + SqliteClientError::AddressGeneration(e) => Some(e), _ => None, } } @@ -146,7 +148,7 @@ impl fmt::Display for SqliteClientError { SqliteClientError::InvalidMemo(e) => write!(f, "{}", e), SqliteClientError::BlockConflict(h) => write!(f, "A block hash conflict occurred at height {}; rewind required.", u32::from(*h)), SqliteClientError::NonSequentialBlocks => write!(f, "`put_blocks` requires that the provided block range be sequential"), - SqliteClientError::DiversifierIndexOutOfRange => write!(f, "The space of available diversifier indices is exhausted"), + SqliteClientError::AddressGeneration(e) => write!(f, "{}", e), SqliteClientError::AccountUnknown(acct_id) => write!(f, "Account {} does not belong to this wallet.", u32::from(*acct_id)), SqliteClientError::KeyDerivationError(acct_id) => write!(f, "Key derivation failed for account {}", u32::from(*acct_id)), @@ -217,3 +219,9 @@ impl From for SqliteClientError { SqliteClientError::BalanceError(e) } } + +impl From for SqliteClientError { + fn from(e: AddressGenerationError) -> Self { + SqliteClientError::AddressGeneration(e) + } +} diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 54f311a584..99f8ab5dda 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -66,7 +66,9 @@ use zcash_client_backend::{ ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, }, - keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, + keys::{ + AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey, + }, proto::compact_formats::CompactBlock, wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput}, DecryptedOutput, ShieldedProtocol, TransferType, @@ -264,8 +266,8 @@ impl, P: consensus::Parameters> WalletRead for W UnifiedAddressRequest::all().map_or(Ok(false), |ua_request| { Ok(usk .to_unified_full_viewing_key() - .default_address(ua_request) - == ufvk.default_address(ua_request)) + .default_address(ua_request)? + == ufvk.default_address(ua_request)?) }) }) }) @@ -450,17 +452,15 @@ impl WalletWrite for WalletDb let search_from = match wallet::get_current_address(wdb.conn.0, &wdb.params, account)? { Some((_, mut last_diversifier_index)) => { - last_diversifier_index - .increment() - .map_err(|_| SqliteClientError::DiversifierIndexOutOfRange)?; + last_diversifier_index.increment().map_err(|_| { + AddressGenerationError::DiversifierSpaceExhausted + })?; last_diversifier_index } None => DiversifierIndex::default(), }; - let (addr, diversifier_index) = ufvk - .find_address(search_from, request) - .ok_or(SqliteClientError::DiversifierIndexOutOfRange)?; + let (addr, diversifier_index) = ufvk.find_address(search_from, request)?; wallet::insert_address( wdb.conn.0, @@ -1238,11 +1238,8 @@ mod tests { #[cfg(feature = "unstable")] use { - crate::testing::AddressType, - zcash_client_backend::keys::sapling, - zcash_primitives::{ - consensus::Parameters, transaction::components::amount::NonNegativeAmount, - }, + crate::testing::AddressType, zcash_client_backend::keys::sapling, + zcash_primitives::transaction::components::amount::NonNegativeAmount, }; #[test] @@ -1318,6 +1315,7 @@ mod tests { // The receiver for the default UA should be in the set. assert!(receivers.contains_key( ufvk.default_address(DEFAULT_UA_REQUEST) + .expect("A valid default address exists for the UFVK") .0 .transparent() .unwrap() @@ -1330,6 +1328,8 @@ mod tests { #[cfg(feature = "unstable")] #[test] pub(crate) fn fsblockdb_api() { + use zcash_primitives::consensus::NetworkConstants; + let mut st = TestBuilder::new().with_fs_block_cache().build(); // The BlockMeta DB starts off empty. diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index d62401bf9b..2a13e468d9 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -54,7 +54,7 @@ use zcash_primitives::{ consensus::{self, BlockHeight, Network, NetworkUpgrade, Parameters}, memo::{Memo, MemoBytes}, transaction::{ - components::amount::NonNegativeAmount, + components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule}, Transaction, TxId, }, @@ -784,11 +784,8 @@ pub(crate) fn fake_compact_block( // Create a fake Note for the account let mut rng = OsRng; - let rseed = generate_random_rseed( - consensus::sapling_zip212_enforcement(params, height), - &mut rng, - ); - let note = Note::from_parts(to, NoteValue::from(value), rseed); + let rseed = generate_random_rseed(zip212_enforcement(params, height), &mut rng); + let note = Note::from_parts(to, NoteValue::from_raw(value.into_u64()), rseed); let encryptor = sapling_note_encryption( Some(dfvk.fvk().ovk), note.clone(), @@ -883,7 +880,7 @@ pub(crate) fn fake_compact_block_spending( value: NonNegativeAmount, initial_sapling_tree_size: u32, ) -> CompactBlock { - let zip212_enforcement = consensus::sapling_zip212_enforcement(params, height); + let zip212_enforcement = zip212_enforcement(params, height); let mut rng = OsRng; let rseed = generate_random_rseed(zip212_enforcement, &mut rng); @@ -897,7 +894,11 @@ pub(crate) fn fake_compact_block_spending( // Create a fake Note for the payment ctx.outputs.push({ - let note = Note::from_parts(to, NoteValue::from(value), rseed); + let note = Note::from_parts( + to, + sapling::value::NoteValue::from_raw(value.into_u64()), + rseed, + ); let encryptor = sapling_note_encryption( Some(dfvk.fvk().ovk), note.clone(), @@ -921,7 +922,7 @@ pub(crate) fn fake_compact_block_spending( let rseed = generate_random_rseed(zip212_enforcement, &mut rng); let note = Note::from_parts( change_addr, - NoteValue::from((in_value - value).unwrap()), + NoteValue::from_raw((in_value - value).unwrap().into_u64()), rseed, ); let encryptor = sapling_note_encryption( diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index f51aa4a53f..29f04f7ea6 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -263,7 +263,9 @@ pub(crate) fn add_account( } // Always derive the default Unified Address for the account. - let (address, d_idx) = key.default_address(DEFAULT_UA_REQUEST); + let (address, d_idx) = key + .default_address(DEFAULT_UA_REQUEST) + .expect("A valid default address exists for the UFVK"); insert_address(conn, params, account, d_idx, &address)?; Ok(()) diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index 14bf46d30f..35734806d9 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -2,21 +2,18 @@ use std::fmt; -use rusqlite::{self}; use schemer::{Migrator, MigratorError}; use schemer_rusqlite::RusqliteAdapter; use secrecy::SecretVec; use shardtree::error::ShardTreeError; use uuid::Uuid; -use zcash_primitives::{ - consensus::{self}, - transaction::components::amount::BalanceError, -}; +use zcash_client_backend::keys::AddressGenerationError; +use zcash_primitives::{consensus, transaction::components::amount::BalanceError}; use crate::WalletDb; -use super::commitment_tree::{self}; +use super::commitment_tree; mod migrations; @@ -28,6 +25,9 @@ pub enum WalletMigrationError { /// Decoding of an existing value from its serialized form has failed. CorruptedData(String), + /// An error occurred in migrating a Zcash address or key. + AddressGeneration(AddressGenerationError), + /// Wrapper for rusqlite errors. DbError(rusqlite::Error), @@ -56,6 +56,12 @@ impl From> for WalletMigrationError { } } +impl From for WalletMigrationError { + fn from(e: AddressGenerationError) -> Self { + WalletMigrationError::AddressGeneration(e) + } +} + impl fmt::Display for WalletMigrationError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { @@ -71,6 +77,9 @@ impl fmt::Display for WalletMigrationError { WalletMigrationError::DbError(e) => write!(f, "{}", e), WalletMigrationError::BalanceError(e) => write!(f, "Balance error: {:?}", e), WalletMigrationError::CommitmentTree(e) => write!(f, "Commitment tree error: {:?}", e), + WalletMigrationError::AddressGeneration(e) => { + write!(f, "Address generation error: {:?}", e) + } } } } @@ -79,6 +88,9 @@ impl std::error::Error for WalletMigrationError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match &self { WalletMigrationError::DbError(e) => Some(e), + WalletMigrationError::BalanceError(e) => Some(e), + WalletMigrationError::CommitmentTree(e) => Some(e), + WalletMigrationError::AddressGeneration(e) => Some(e), _ => None, } } @@ -176,7 +188,9 @@ mod tests { use ::sapling::zip32::ExtendedFullViewingKey; use zcash_primitives::{ - consensus::{self, BlockHeight, BranchId, Network, NetworkUpgrade, Parameters}, + consensus::{ + self, BlockHeight, BranchId, Network, NetworkConstants, NetworkUpgrade, Parameters, + }, transaction::{TransactionData, TxVersion}, zip32::AccountId, }; @@ -1007,8 +1021,12 @@ mod tests { )?; let ufvk_str = ufvk.encode(&wdb.params); - let address_str = - Address::Unified(ufvk.default_address(DEFAULT_UA_REQUEST).0).encode(&wdb.params); + let address_str = Address::Unified( + ufvk.default_address(DEFAULT_UA_REQUEST) + .expect("A valid default address exists for the UFVK") + .0, + ) + .encode(&wdb.params); wdb.conn.execute( "INSERT INTO accounts (account, ufvk, address, transparent_address) VALUES (?, ?, ?, '')", @@ -1025,6 +1043,7 @@ mod tests { let taddr = Address::Transparent( *ufvk .default_address(DEFAULT_UA_REQUEST) + .expect("A valid default address exists for the UFVK") .0 .transparent() .unwrap(), diff --git a/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs index 5dda3f1977..d3eed031e1 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs @@ -441,11 +441,13 @@ mod tests { let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap(); let ufvk = usk.to_unified_full_viewing_key(); - let (ua, _) = ufvk.default_address(UnifiedAddressRequest::unsafe_new( - false, - true, - UA_TRANSPARENT, - )); + let (ua, _) = ufvk + .default_address(UnifiedAddressRequest::unsafe_new( + false, + true, + UA_TRANSPARENT, + )) + .expect("A valid default address exists for the UFVK"); let taddr = ufvk .transparent() .and_then(|k| { diff --git a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs index 353da97b27..8c3490736c 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs @@ -88,11 +88,9 @@ impl RusqliteMigration for Migration

{ "Address in accounts table was not a Unified Address.".to_string(), )); }; - let (expected_address, idx) = ufvk.default_address(UnifiedAddressRequest::unsafe_new( - false, - true, - UA_TRANSPARENT, - )); + let (expected_address, idx) = ufvk.default_address( + UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT), + )?; if decoded_address != expected_address { return Err(WalletMigrationError::CorruptedData(format!( "Decoded UA {} does not match the UFVK's default address {} at {:?}.", @@ -166,7 +164,7 @@ impl RusqliteMigration for Migration

{ false, true, UA_TRANSPARENT, - )); + ))?; insert_address(transaction, &self.params, account, d_idx, &address)?; } diff --git a/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs index 366d946d27..c45697e34b 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs @@ -18,8 +18,11 @@ use sapling::{ }; use zcash_client_backend::{data_api::SAPLING_SHARD_HEIGHT, keys::UnifiedFullViewingKey}; use zcash_primitives::{ - consensus::{self, sapling_zip212_enforcement, BlockHeight, BranchId}, - transaction::{components::amount::NonNegativeAmount, Transaction}, + consensus::{self, BlockHeight, BranchId}, + transaction::{ + components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, + Transaction, + }, zip32::Scope, }; @@ -165,21 +168,29 @@ impl RusqliteMigration for Migration

{ // be mined under ZIP 212 enforcement rules, so we default to `On` Zip212Enforcement::On }, - |h| sapling_zip212_enforcement(&self.params, h), + |h| zip212_enforcement(&self.params, h), ); let ufvk_str: String = row.get(5)?; - let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str) - .expect("Stored UFVKs must be valid"); - let dfvk = ufvk - .sapling() - .expect("UFVK must have a Sapling component to have received Sapling notes"); + let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str).map_err(|e| { + WalletMigrationError::CorruptedData(format!("Stored UFVK was invalid: {:?}", e)) + })?; + + let dfvk = ufvk.sapling().ok_or_else(|| { + WalletMigrationError::CorruptedData( + "UFVK must have a Sapling component to have received Sapling notes.".to_owned(), + ) + })?; // We previously set the default to external scope, so we now verify whether the output // is decryptable using the intenally-scoped IVK and, if so, mark it as such. if let Some(tx_data) = tx_data_opt { - let tx = Transaction::read(&tx_data[..], BranchId::Canopy) - .expect("Transaction must be valid"); + let tx = Transaction::read(&tx_data[..], BranchId::Canopy).map_err(|e| { + WalletMigrationError::CorruptedData(format!( + "Unable to parse raw transaction: {:?}", + e + )) + })?; let output = tx .sapling_bundle() .and_then(|b| b.shielded_outputs().get(output_index)) @@ -241,7 +252,7 @@ impl RusqliteMigration for Migration

{ &mut commitment_tree, dfvk, &diversifier, - ¬e_value.try_into().unwrap(), + &sapling::value::NoteValue::from_raw(note_value.into_u64()), &rseed, note_commitment_tree_position, )?; diff --git a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs index 7e5f7b6e4c..99c27b5b33 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs @@ -68,8 +68,7 @@ impl RusqliteMigration for Migration

{ let mut stmt_fetch_accounts = transaction.prepare("SELECT account, address FROM accounts")?; - let ua_request = UnifiedAddressRequest::new(false, true, UA_TRANSPARENT) - .expect("A shielded receiver type is requested."); + let ua_request = UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT); let mut rows = stmt_fetch_accounts.query([])?; while let Some(row) = rows.next()? { // We only need to check for the presence of the seed if we have keys that @@ -94,9 +93,8 @@ impl RusqliteMigration for Migration

{ })?; match decoded { Address::Sapling(decoded_address) => { - let dfvk = ufvk.sapling().expect( - "Derivation should have produced a UFVK containing a Sapling component.", - ); + let dfvk = ufvk.sapling().ok_or_else(|| + WalletMigrationError::CorruptedData("Derivation should have produced a UFVK containing a Sapling component.".to_owned()))?; let (idx, expected_address) = dfvk.default_address(); if decoded_address != expected_address { return Err(WalletMigrationError::CorruptedData( @@ -111,7 +109,7 @@ impl RusqliteMigration for Migration

{ "Address field value decoded to a transparent address; should have been Sapling or unified.".to_string())); } Address::Unified(decoded_address) => { - let (expected_address, idx) = ufvk.default_address(ua_request); + let (expected_address, idx) = ufvk.default_address(ua_request)?; if decoded_address != expected_address { return Err(WalletMigrationError::CorruptedData( format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.", @@ -123,7 +121,7 @@ impl RusqliteMigration for Migration

{ } let ufvk_str: String = ufvk.encode(&self.params); - let address_str: String = ufvk.default_address(ua_request).0.encode(&self.params); + let address_str: String = ufvk.default_address(ua_request)?.0.encode(&self.params); // This migration, and the wallet behaviour before it, stored the default // transparent address in the `accounts` table. This does not necessarily diff --git a/zcash_client_sqlite/src/wallet/sapling.rs b/zcash_client_sqlite/src/wallet/sapling.rs index 26a90f13a9..74270b160e 100644 --- a/zcash_client_sqlite/src/wallet/sapling.rs +++ b/zcash_client_sqlite/src/wallet/sapling.rs @@ -9,10 +9,7 @@ use sapling::{self, Diversifier, Nullifier, Rseed}; use zcash_primitives::{ consensus::{self, BlockHeight}, memo::MemoBytes, - transaction::{ - components::{amount::NonNegativeAmount, Amount}, - TxId, - }, + transaction::{components::Amount, TxId}, zip32::{AccountId, Scope}, }; @@ -115,7 +112,7 @@ fn to_spendable_note( Diversifier(tmp) }; - let note_value = NonNegativeAmount::from_nonnegative_i64(row.get(4)?).map_err(|_e| { + let note_value: u64 = row.get::<_, i64>(4)?.try_into().map_err(|_e| { SqliteClientError::CorruptedData("Note values must be nonnegative".to_string()) })?; @@ -164,7 +161,7 @@ fn to_spendable_note( output_index, Note::Sapling(sapling::Note::from_parts( recipient, - note_value.into(), + sapling::value::NoteValue::from_raw(note_value), rseed, )), spending_key_scope, @@ -484,11 +481,11 @@ pub(crate) mod tests { }; use zcash_primitives::{ block::BlockHash, - consensus::{sapling_zip212_enforcement, BranchId}, + consensus::BranchId, legacy::TransparentAddress, memo::{Memo, MemoBytes}, transaction::{ - components::{amount::NonNegativeAmount, Amount}, + components::{amount::NonNegativeAmount, sapling::zip212_enforcement, Amount}, fees::{ fixed::FeeRule as FixedFeeRule, zip317::FeeError as Zip317FeeError, StandardFeeRule, }, @@ -1261,7 +1258,7 @@ pub(crate) mod tests { let result = try_sapling_output_recovery( &dfvk.to_ovk(Scope::External), output, - sapling_zip212_enforcement(&st.network(), h1), + zip212_enforcement(&st.network(), h1), ); if result.is_some() { diff --git a/zcash_extensions/src/transparent/demo.rs b/zcash_extensions/src/transparent/demo.rs index 96f2d9f6fa..6f26629932 100644 --- a/zcash_extensions/src/transparent/demo.rs +++ b/zcash_extensions/src/transparent/demo.rs @@ -484,8 +484,7 @@ mod tests { use sapling::{zip32::ExtendedSpendingKey, Node, Rseed}; use zcash_primitives::{ - consensus::{BlockHeight, BranchId, NetworkUpgrade, Parameters}, - constants, + consensus::{BlockHeight, BranchId, NetworkType, NetworkUpgrade, Parameters}, extensions::transparent::{self as tze, Extension, FromPayload, ToPayload}, legacy::TransparentAddress, transaction::{ @@ -520,34 +519,11 @@ mod tests { } } - fn address_network(&self) -> Option { - None - } - - fn coin_type(&self) -> u32 { - constants::testnet::COIN_TYPE - } - - fn hrp_sapling_extended_spending_key(&self) -> &str { - constants::testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY - } - - fn hrp_sapling_extended_full_viewing_key(&self) -> &str { - constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY - } - - fn hrp_sapling_payment_address(&self) -> &str { - constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS - } - - fn b58_pubkey_address_prefix(&self) -> [u8; 2] { - constants::testnet::B58_PUBKEY_ADDRESS_PREFIX - } - - fn b58_script_address_prefix(&self) -> [u8; 2] { - constants::testnet::B58_SCRIPT_ADDRESS_PREFIX + fn network_type(&self) -> NetworkType { + NetworkType::Test } } + fn demo_hashes(preimage_1: &[u8; 32], preimage_2: &[u8; 32]) -> ([u8; 32], [u8; 32]) { let hash_2 = { let mut hash = [0; 32]; diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md index 4ebf3c1c38..5689f98bbc 100644 --- a/zcash_keys/CHANGELOG.md +++ b/zcash_keys/CHANGELOG.md @@ -6,6 +6,23 @@ and this library adheres to Rust's notion of ## [Unreleased] +### Added +- `zcash_keys::address::Address::has_receiver` +- `impl Display for zcash_keys::keys::AddressGenerationError` +- `impl std::error::Error for zcash_keys::keys::AddressGenerationError` + +### Changed +- `zcash_keys::keys::AddressGenerationError` has a new variant + `DiversifierSpaceExhausted`. +- `zcash_keys::keys::UnifiedFullViewingKey::{find_address, default_address}` + now return `Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError>` + (instead of `Option<(UnifiedAddress, DiversifierIndex)>` for `find_address`). + +### Fixed +- `UnifiedFullViewingKey::find_address` can now find an address for a diversifier + index outside the valid transparent range if you aren't requesting a + transparent receiver. + ## [0.1.1] - 2024-03-04 ### Added diff --git a/zcash_keys/Cargo.toml b/zcash_keys/Cargo.toml index 5d3f8cdd91..dd08483f8d 100644 --- a/zcash_keys/Cargo.toml +++ b/zcash_keys/Cargo.toml @@ -22,6 +22,7 @@ rustdoc-args = ["--cfg", "docsrs"] zcash_address.workspace = true zcash_encoding.workspace = true zcash_primitives.workspace = true +zcash_protocol.workspace = true zip32.workspace = true # Dependencies exposed in a public API: diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs index c0990c24ba..dd1fb9ac43 100644 --- a/zcash_keys/src/address.rs +++ b/zcash_keys/src/address.rs @@ -2,12 +2,14 @@ use zcash_address::{ unified::{self, Container, Encoding, Typecode}, - ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress, + ConversionError, ToAddress, TryFromRawAddress, ZcashAddress, }; -use zcash_primitives::{consensus, legacy::TransparentAddress}; +use zcash_primitives::legacy::TransparentAddress; +use zcash_protocol::consensus::{self, NetworkType}; #[cfg(feature = "sapling")] use sapling::PaymentAddress; +use zcash_protocol::{PoolType, ShieldedProtocol}; /// A Unified Address. #[derive(Clone, Debug, PartialEq, Eq)] @@ -171,7 +173,7 @@ impl UnifiedAddress { &self.unknown } - fn to_address(&self, net: Network) -> ZcashAddress { + fn to_address(&self, net: NetworkType) -> ZcashAddress { let items = self .unknown .iter() @@ -208,8 +210,7 @@ impl UnifiedAddress { /// Returns the string encoding of this `UnifiedAddress` for the given network. pub fn encode(&self, params: &P) -> String { - self.to_address(params.address_network().expect("Unrecognized network")) - .to_string() + self.to_address(params.network_type()).to_string() } /// Returns the set of receiver typecodes. @@ -291,12 +292,11 @@ impl TryFromRawAddress for Address { impl Address { pub fn decode(params: &P, s: &str) -> Option { let addr = ZcashAddress::try_from_encoded(s).ok()?; - addr.convert_if_network(params.address_network().expect("Unrecognized network")) - .ok() + addr.convert_if_network(params.network_type()).ok() } pub fn encode(&self, params: &P) -> String { - let net = params.address_network().expect("Unrecognized network"); + let net = params.network_type(); match self { #[cfg(feature = "sapling")] @@ -313,6 +313,33 @@ impl Address { } .to_string() } + + pub fn has_receiver(&self, pool_type: PoolType) -> bool { + match self { + #[cfg(feature = "sapling")] + Address::Sapling(_) => { + matches!(pool_type, PoolType::Shielded(ShieldedProtocol::Sapling)) + } + Address::Transparent(_) => matches!(pool_type, PoolType::Transparent), + Address::Unified(ua) => match pool_type { + PoolType::Transparent => ua.transparent().is_some(), + PoolType::Shielded(ShieldedProtocol::Sapling) => { + #[cfg(feature = "sapling")] + return ua.sapling().is_some(); + + #[cfg(not(feature = "sapling"))] + return false; + } + PoolType::Shielded(ShieldedProtocol::Orchard) => { + #[cfg(feature = "orchard")] + return ua.orchard().is_some(); + + #[cfg(not(feature = "orchard"))] + return false; + } + }, + } + } } #[cfg(any(test, feature = "test-dependencies"))] diff --git a/zcash_keys/src/encoding.rs b/zcash_keys/src/encoding.rs index 7a280178c6..8de7125a05 100644 --- a/zcash_keys/src/encoding.rs +++ b/zcash_keys/src/encoding.rs @@ -6,6 +6,7 @@ use crate::address::UnifiedAddress; use bs58::{self, decode::Error as Bs58Error}; use std::fmt; +use zcash_primitives::consensus::NetworkConstants; use zcash_address::unified::{self, Encoding}; use zcash_primitives::{consensus, legacy::TransparentAddress}; @@ -173,7 +174,7 @@ impl AddressCodec

for UnifiedAddress { unified::Address::decode(address) .map_err(|e| format!("{}", e)) .and_then(|(network, addr)| { - if params.address_network() == Some(network) { + if params.network_type() == network { UnifiedAddress::try_from(addr).map_err(|e| e.to_owned()) } else { Err(format!( @@ -312,7 +313,7 @@ pub fn encode_payment_address_p( /// encoding::decode_payment_address, /// }; /// use zcash_primitives::{ -/// consensus::{TEST_NETWORK, Parameters}, +/// consensus::{TEST_NETWORK, NetworkConstants, Parameters}, /// }; /// /// let pa = PaymentAddress::from_bytes(&[ @@ -357,7 +358,7 @@ pub fn decode_payment_address( /// encoding::encode_transparent_address, /// }; /// use zcash_primitives::{ -/// consensus::{TEST_NETWORK, Parameters}, +/// consensus::{TEST_NETWORK, NetworkConstants, Parameters}, /// legacy::TransparentAddress, /// }; /// @@ -422,12 +423,12 @@ pub fn encode_transparent_address_p( /// /// ``` /// use zcash_primitives::{ -/// consensus::{TEST_NETWORK, Parameters}, +/// consensus::{TEST_NETWORK, NetworkConstants, Parameters}, +/// legacy::TransparentAddress, /// }; /// use zcash_keys::{ /// encoding::decode_transparent_address, /// }; -/// use zcash_primitives::legacy::TransparentAddress; /// /// assert_eq!( /// decode_transparent_address( diff --git a/zcash_keys/src/keys.rs b/zcash_keys/src/keys.rs index 3515f4cbf9..147b33cb18 100644 --- a/zcash_keys/src/keys.rs +++ b/zcash_keys/src/keys.rs @@ -1,9 +1,9 @@ //! Helper functions for managing light client key material. +use std::{error, fmt}; + use zcash_address::unified::{self, Container, Encoding, Typecode}; -use zcash_primitives::{ - consensus, - zip32::{AccountId, DiversifierIndex}, -}; +use zcash_protocol::consensus::{self, NetworkConstants}; +use zip32::{AccountId, DiversifierIndex}; use crate::address::UnifiedAddress; @@ -404,7 +404,9 @@ impl UnifiedSpendingKey { &self, request: UnifiedAddressRequest, ) -> (UnifiedAddress, DiversifierIndex) { - self.to_unified_full_viewing_key().default_address(request) + self.to_unified_full_viewing_key() + .default_address(request) + .unwrap() } #[cfg(all( @@ -430,6 +432,8 @@ pub enum AddressGenerationError { /// The diversifier index could not be mapped to a valid Sapling diversifier. #[cfg(feature = "sapling")] InvalidSaplingDiversifierIndex(DiversifierIndex), + /// The space of available diversifier indices has been exhausted. + DiversifierSpaceExhausted, /// A requested address typecode was not recognized, so we are unable to generate the address /// as requested. ReceiverTypeNotSupported(Typecode), @@ -441,6 +445,53 @@ pub enum AddressGenerationError { ShieldedReceiverRequired, } +impl fmt::Display for AddressGenerationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + #[cfg(feature = "transparent-inputs")] + AddressGenerationError::InvalidTransparentChildIndex(i) => { + write!( + f, + "Child index {:?} does not generate a valid transparent receiver", + i + ) + } + AddressGenerationError::InvalidSaplingDiversifierIndex(i) => { + write!( + f, + "Child index {:?} does not generate a valid Sapling receiver", + i + ) + } + AddressGenerationError::DiversifierSpaceExhausted => { + write!( + f, + "Exhausted the space of diversifier indices without finding an address." + ) + } + AddressGenerationError::ReceiverTypeNotSupported(t) => { + write!( + f, + "Unified Address generation does not yet support receivers of type {:?}.", + t + ) + } + AddressGenerationError::KeyNotAvailable(t) => { + write!( + f, + "The Unified Viewing Key does not contain a key for typecode {:?}.", + t + ) + } + AddressGenerationError::ShieldedReceiverRequired => { + write!(f, "A Unified Address requires at least one shielded (Sapling or Orchard) receiver.") + } + } + } +} + +impl error::Error for AddressGenerationError {} + /// Specification for how a unified address should be generated from a unified viewing key. #[derive(Clone, Copy, Debug)] pub struct UnifiedAddressRequest { @@ -554,7 +605,7 @@ impl UnifiedFullViewingKey { /// [ZIP 316]: https://zips.z.cash/zip-0316 pub fn decode(params: &P, encoding: &str) -> Result { let (net, ufvk) = unified::Ufvk::decode(encoding).map_err(|e| e.to_string())?; - let expected_net = params.address_network().expect("Unrecognized network"); + let expected_net = params.network_type(); if net != expected_net { return Err(format!( "UFVK is for network {:?} but we expected {:?}", @@ -663,7 +714,7 @@ impl UnifiedFullViewingKey { let ufvk = unified::Ufvk::try_from_items(items.collect()) .expect("UnifiedFullViewingKey should only be constructed safely"); - ufvk.encode(¶ms.address_network().expect("Unrecognized network")) + ufvk.encode(¶ms.network_type()) } /// Returns the transparent component of the unified key at the @@ -776,19 +827,23 @@ impl UnifiedFullViewingKey { /// produce a valid diversifier, and return the Unified Address constructed using that /// diversifier along with the index at which the valid diversifier was found. /// - /// Returns `None` if no valid diversifier exists + /// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features + /// required to satisfy the unified address request are not properly enabled. #[allow(unused_mut)] pub fn find_address( &self, mut j: DiversifierIndex, request: UnifiedAddressRequest, - ) -> Option<(UnifiedAddress, DiversifierIndex)> { + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { // If we need to generate a transparent receiver, check that the user has not // specified an invalid transparent child index, from which we can never search to // find a valid index. #[cfg(feature = "transparent-inputs")] - if self.transparent.is_some() && to_transparent_child_index(j).is_none() { - return None; + if request.has_p2pkh + && self.transparent.is_some() + && to_transparent_child_index(j).is_none() + { + return Err(AddressGenerationError::InvalidTransparentChildIndex(j)); } // Find a working diversifier and construct the associated address. @@ -796,29 +851,31 @@ impl UnifiedFullViewingKey { let res = self.address(j, request); match res { Ok(ua) => { - break Some((ua, j)); + return Ok((ua, j)); } #[cfg(feature = "sapling")] Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_)) => { if j.increment().is_err() { - break None; + return Err(AddressGenerationError::DiversifierSpaceExhausted); } } - Err(_) => { - break None; + Err(other) => { + return Err(other); } } } } - /// Returns the Unified Address corresponding to the smallest valid diversifier index, - /// along with that index. + /// Find the Unified Address corresponding to the smallest valid diversifier index, along with + /// that index. + /// + /// Returns an `Err(AddressGenerationError)` if no valid diversifier exists or if the features + /// required to satisfy the unified address request are not properly enabled. pub fn default_address( &self, request: UnifiedAddressRequest, - ) -> (UnifiedAddress, DiversifierIndex) { + ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { self.find_address(DiversifierIndex::new(), request) - .expect("UFVK should have at least one valid diversifier") } } diff --git a/zcash_primitives/CHANGELOG.md b/zcash_primitives/CHANGELOG.md index e9afe9dc0a..35723435aa 100644 --- a/zcash_primitives/CHANGELOG.md +++ b/zcash_primitives/CHANGELOG.md @@ -7,6 +7,33 @@ and this library adheres to Rust's notion of ## [Unreleased] +### Added +- `zcash_primitives::transaction::components::sapling::zip212_enforcement` + +### Changed +- The following modules are now re-exported from the `zcash_protocol` crate. + Additional changes have also been made therein; refer to the `zcash_protocol` + changelog for details. + - `zcash_primitives::consensus` re-exports `zcash_protocol::consensus`. + - `zcash_primitives::constants` re-exports `zcash_protocol::constants`. + - `zcash_primitives::transaction::components::amount` re-exports + `zcash_protocol::value`. Many of the conversions to and from the + `Amount` and `NonNegativeAmount` value types now return + `Result<_, BalanceError>` instead of `Result<_, ()>`. + - `zcash_primitives::memo` re-exports `zcash_protocol::memo`. + +### Removed +- `zcash_primitives::consensus::sapling_zip212_enforcement` instead use + `zcash_primitives::transaction::components::sapling::zip212_enforcement`. +- From `zcash_primitive::components::transaction`: + - `impl From for u64` + - `impl TryFrom for NonNegativeAmount` + - `impl From for sapling::value::NoteValue` + - `impl TryFrom for Amount` + - `impl From for orchard::NoteValue` +- The `local_consensus` module and feature flag have been removed; use the module + from the `zcash_protocol` crate instead. + ## [0.14.0] - 2024-03-01 ### Added - Dependency on `bellman 0.14`, `sapling-crypto 0.1`. diff --git a/zcash_primitives/Cargo.toml b/zcash_primitives/Cargo.toml index 95c1bdfe1b..c349215600 100644 --- a/zcash_primitives/Cargo.toml +++ b/zcash_primitives/Cargo.toml @@ -22,6 +22,7 @@ rustdoc-args = ["--cfg", "docsrs"] equihash.workspace = true zcash_address.workspace = true zcash_encoding.workspace = true +zcash_protocol.workspace = true zip32.workspace = true # Dependencies exposed in a public API: @@ -119,6 +120,7 @@ test-dependencies = [ "dep:proptest", "orchard/test-dependencies", "sapling/test-dependencies", + "zcash_protocol/test-dependencies", ] #! ### Experimental features @@ -127,13 +129,10 @@ test-dependencies = [ #! consensus rules! ## Exposes the in-development NU6 features. -unstable-nu6 = [] +unstable-nu6 = ["zcash_protocol/unstable-nu6"] ## Exposes early in-development features that are not yet planned for any network upgrade. -zfuture = [] - -## Exposes support for working with a local consensus (e.g. regtest -local-consensus = [] +zfuture = ["zcash_protocol/zfuture"] [lib] bench = false diff --git a/zcash_primitives/benches/note_decryption.rs b/zcash_primitives/benches/note_decryption.rs index 518f9623c3..99048cb843 100644 --- a/zcash_primitives/benches/note_decryption.rs +++ b/zcash_primitives/benches/note_decryption.rs @@ -15,8 +15,8 @@ use sapling::{ }; use zcash_note_encryption::batch; use zcash_primitives::{ - consensus::{sapling_zip212_enforcement, NetworkUpgrade::Canopy, Parameters, TEST_NETWORK}, - transaction::components::Amount, + consensus::{NetworkUpgrade::Canopy, Parameters, TEST_NETWORK}, + transaction::components::{sapling::zip212_enforcement, Amount}, }; #[cfg(unix)] @@ -25,7 +25,7 @@ use pprof::criterion::{Output, PProfProfiler}; fn bench_note_decryption(c: &mut Criterion) { let mut rng = OsRng; let height = TEST_NETWORK.activation_height(Canopy).unwrap(); - let zip212_enforcement = sapling_zip212_enforcement(&TEST_NETWORK, height); + let zip212_enforcement = zip212_enforcement(&TEST_NETWORK, height); let valid_ivk = SaplingIvk(jubjub::Fr::random(&mut rng)); let invalid_ivk = SaplingIvk(jubjub::Fr::random(&mut rng)); diff --git a/zcash_primitives/src/legacy/keys.rs b/zcash_primitives/src/legacy/keys.rs index ff4f7e946a..fbed55028e 100644 --- a/zcash_primitives/src/legacy/keys.rs +++ b/zcash_primitives/src/legacy/keys.rs @@ -7,9 +7,10 @@ use hdwallet::{ use secp256k1::PublicKey; use sha2::{Digest, Sha256}; use subtle::{Choice, ConstantTimeEq}; -use zcash_spec::PrfExpand; -use crate::{consensus, zip32::AccountId}; +use zcash_protocol::consensus::{self, NetworkConstants}; +use zcash_spec::PrfExpand; +use zip32::AccountId; use super::TransparentAddress; diff --git a/zcash_primitives/src/lib.rs b/zcash_primitives/src/lib.rs index e32e2165bc..37c37d653a 100644 --- a/zcash_primitives/src/lib.rs +++ b/zcash_primitives/src/lib.rs @@ -18,16 +18,14 @@ #![allow(clippy::single_component_path_imports)] pub mod block; -pub mod consensus; -pub mod constants; +pub use zcash_protocol::consensus; +pub use zcash_protocol::constants; pub mod legacy; -pub mod memo; +pub use zcash_protocol::memo; pub mod merkle_tree; use sapling; pub mod transaction; pub use zip32; #[cfg(feature = "zfuture")] pub mod extensions; -#[cfg(feature = "local-consensus")] -pub mod local_consensus; pub mod zip339; diff --git a/zcash_primitives/src/transaction/builder.rs b/zcash_primitives/src/transaction/builder.rs index 95e051536c..2fc016bd8f 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -48,6 +48,7 @@ use crate::{ }; use super::components::amount::NonNegativeAmount; +use super::components::sapling::zip212_enforcement; /// Since Blossom activation, the default transaction expiry delta should be 40 blocks. /// @@ -352,7 +353,7 @@ impl<'a, P: consensus::Parameters> Builder<'a, P, ()> { .sapling_builder_config() .map(|(bundle_type, anchor)| { sapling::builder::Builder::new( - consensus::sapling_zip212_enforcement(¶ms, target_height), + zip212_enforcement(¶ms, target_height), bundle_type, anchor, ) diff --git a/zcash_primitives/src/transaction/components.rs b/zcash_primitives/src/transaction/components.rs index 24dfd724b5..bba5ddf951 100644 --- a/zcash_primitives/src/transaction/components.rs +++ b/zcash_primitives/src/transaction/components.rs @@ -1,6 +1,17 @@ //! Structs representing the components within Zcash transactions. +pub mod amount { + pub use zcash_protocol::value::{ + BalanceError, ZatBalance as Amount, Zatoshis as NonNegativeAmount, COIN, + }; -pub mod amount; + #[cfg(feature = "test-dependencies")] + pub mod testing { + pub use zcash_protocol::value::testing::{ + arb_positive_zat_balance as arb_positive_amount, arb_zat_balance as arb_amount, + arb_zatoshis as arb_nonnegative_amount, + }; + } +} pub mod orchard; pub mod sapling; pub mod sprout; diff --git a/zcash_primitives/src/transaction/components/amount.rs b/zcash_primitives/src/transaction/components/amount.rs deleted file mode 100644 index 766368202c..0000000000 --- a/zcash_primitives/src/transaction/components/amount.rs +++ /dev/null @@ -1,557 +0,0 @@ -use std::convert::{Infallible, TryFrom}; -use std::error; -use std::iter::Sum; -use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}; - -use memuse::DynamicUsage; -use orchard::value as orchard; - -use crate::sapling; - -pub const COIN: i64 = 1_0000_0000; -pub const MAX_MONEY: i64 = 21_000_000 * COIN; - -/// A type-safe representation of a Zcash value delta, in zatoshis. -/// -/// An Amount can only be constructed from an integer that is within the valid monetary -/// range of `{-MAX_MONEY..MAX_MONEY}` (where `MAX_MONEY` = 21,000,000 × 10⁸ zatoshis). -/// However, this range is not preserved as an invariant internally; it is possible to -/// add two valid Amounts together to obtain an invalid Amount. It is the user's -/// responsibility to handle the result of serializing potentially-invalid Amounts. In -/// particular, a [`Transaction`] containing serialized invalid Amounts will be rejected -/// by the network consensus rules. -/// -/// [`Transaction`]: crate::transaction::Transaction -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)] -pub struct Amount(i64); - -memuse::impl_no_dynamic_usage!(Amount); - -impl Amount { - /// Returns a zero-valued Amount. - pub const fn zero() -> Self { - Amount(0) - } - - /// Creates a constant Amount from an i64. - /// - /// Panics: if the amount is outside the range `{-MAX_MONEY..MAX_MONEY}`. - pub const fn const_from_i64(amount: i64) -> Self { - assert!(-MAX_MONEY <= amount && amount <= MAX_MONEY); // contains is not const - Amount(amount) - } - - /// Creates a constant Amount from a u64. - /// - /// Panics: if the amount is outside the range `{0..MAX_MONEY}`. - const fn const_from_u64(amount: u64) -> Self { - assert!(amount <= (MAX_MONEY as u64)); // contains is not const - Amount(amount as i64) - } - - /// Creates an Amount from an i64. - /// - /// Returns an error if the amount is outside the range `{-MAX_MONEY..MAX_MONEY}`. - pub fn from_i64(amount: i64) -> Result { - if (-MAX_MONEY..=MAX_MONEY).contains(&amount) { - Ok(Amount(amount)) - } else { - Err(()) - } - } - - /// Creates a non-negative Amount from an i64. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_nonnegative_i64(amount: i64) -> Result { - if (0..=MAX_MONEY).contains(&amount) { - Ok(Amount(amount)) - } else { - Err(()) - } - } - - /// Creates an Amount from a u64. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_u64(amount: u64) -> Result { - if amount <= MAX_MONEY as u64 { - Ok(Amount(amount as i64)) - } else { - Err(()) - } - } - - /// Reads an Amount from a signed 64-bit little-endian integer. - /// - /// Returns an error if the amount is outside the range `{-MAX_MONEY..MAX_MONEY}`. - pub fn from_i64_le_bytes(bytes: [u8; 8]) -> Result { - let amount = i64::from_le_bytes(bytes); - Amount::from_i64(amount) - } - - /// Reads a non-negative Amount from a signed 64-bit little-endian integer. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_nonnegative_i64_le_bytes(bytes: [u8; 8]) -> Result { - let amount = i64::from_le_bytes(bytes); - Amount::from_nonnegative_i64(amount) - } - - /// Reads an Amount from an unsigned 64-bit little-endian integer. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_u64_le_bytes(bytes: [u8; 8]) -> Result { - let amount = u64::from_le_bytes(bytes); - Amount::from_u64(amount) - } - - /// Returns the Amount encoded as a signed 64-bit little-endian integer. - pub fn to_i64_le_bytes(self) -> [u8; 8] { - self.0.to_le_bytes() - } - - /// Returns `true` if `self` is positive and `false` if the Amount is zero or - /// negative. - pub const fn is_positive(self) -> bool { - self.0.is_positive() - } - - /// Returns `true` if `self` is negative and `false` if the Amount is zero or - /// positive. - pub const fn is_negative(self) -> bool { - self.0.is_negative() - } - - pub fn sum>(values: I) -> Option { - let mut result = Amount::zero(); - for value in values { - result = (result + value)?; - } - Some(result) - } -} - -impl TryFrom for Amount { - type Error = (); - - fn try_from(value: i64) -> Result { - Amount::from_i64(value) - } -} - -impl From for i64 { - fn from(amount: Amount) -> i64 { - amount.0 - } -} - -impl From<&Amount> for i64 { - fn from(amount: &Amount) -> i64 { - amount.0 - } -} - -impl TryFrom for u64 { - type Error = (); - - fn try_from(value: Amount) -> Result { - value.0.try_into().map_err(|_| ()) - } -} - -impl Add for Amount { - type Output = Option; - - fn add(self, rhs: Amount) -> Option { - Amount::from_i64(self.0 + rhs.0).ok() - } -} - -impl Add for Option { - type Output = Self; - - fn add(self, rhs: Amount) -> Option { - self.and_then(|lhs| lhs + rhs) - } -} - -impl AddAssign for Amount { - fn add_assign(&mut self, rhs: Amount) { - *self = (*self + rhs).expect("Addition must produce a valid amount value.") - } -} - -impl Sub for Amount { - type Output = Option; - - fn sub(self, rhs: Amount) -> Option { - Amount::from_i64(self.0 - rhs.0).ok() - } -} - -impl Sub for Option { - type Output = Self; - - fn sub(self, rhs: Amount) -> Option { - self.and_then(|lhs| lhs - rhs) - } -} - -impl SubAssign for Amount { - fn sub_assign(&mut self, rhs: Amount) { - *self = (*self - rhs).expect("Subtraction must produce a valid amount value.") - } -} - -impl Sum for Option { - fn sum>(iter: I) -> Self { - iter.fold(Some(Amount::zero()), |acc, a| acc? + a) - } -} - -impl<'a> Sum<&'a Amount> for Option { - fn sum>(iter: I) -> Self { - iter.fold(Some(Amount::zero()), |acc, a| acc? + *a) - } -} - -impl Neg for Amount { - type Output = Self; - - fn neg(self) -> Self { - Amount(-self.0) - } -} - -impl Mul for Amount { - type Output = Option; - - fn mul(self, rhs: usize) -> Option { - let rhs: i64 = rhs.try_into().ok()?; - self.0 - .checked_mul(rhs) - .and_then(|i| Amount::try_from(i).ok()) - } -} - -impl TryFrom for Amount { - type Error = (); - - fn try_from(v: orchard::ValueSum) -> Result { - i64::try_from(v).map_err(|_| ()).and_then(Amount::try_from) - } -} - -/// A type-safe representation of some nonnegative amount of Zcash. -/// -/// A NonNegativeAmount can only be constructed from an integer that is within the valid monetary -/// range of `{0..MAX_MONEY}` (where `MAX_MONEY` = 21,000,000 × 10⁸ zatoshis). -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)] -pub struct NonNegativeAmount(Amount); - -impl NonNegativeAmount { - /// Returns the identity `NonNegativeAmount` - pub const ZERO: Self = NonNegativeAmount(Amount(0)); - - /// Creates a NonNegativeAmount from a u64. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_u64(amount: u64) -> Result { - Amount::from_u64(amount).map(NonNegativeAmount) - } - - /// Creates a constant NonNegativeAmount from a u64. - /// - /// Panics: if the amount is outside the range `{-MAX_MONEY..MAX_MONEY}`. - pub const fn const_from_u64(amount: u64) -> Self { - NonNegativeAmount(Amount::const_from_u64(amount)) - } - - /// Creates a NonNegativeAmount from an i64. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_nonnegative_i64(amount: i64) -> Result { - Amount::from_nonnegative_i64(amount).map(NonNegativeAmount) - } - - /// Reads an NonNegativeAmount from an unsigned 64-bit little-endian integer. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_u64_le_bytes(bytes: [u8; 8]) -> Result { - let amount = u64::from_le_bytes(bytes); - Self::from_u64(amount) - } - - /// Reads a NonNegativeAmount from a signed integer represented as a two's - /// complement 64-bit little-endian value. - /// - /// Returns an error if the amount is outside the range `{0..MAX_MONEY}`. - pub fn from_nonnegative_i64_le_bytes(bytes: [u8; 8]) -> Result { - let amount = i64::from_le_bytes(bytes); - Self::from_nonnegative_i64(amount) - } - - /// Returns this NonNegativeAmount encoded as a signed two's complement 64-bit - /// little-endian value. - pub fn to_i64_le_bytes(self) -> [u8; 8] { - self.0.to_i64_le_bytes() - } - - /// Returns whether or not this `NonNegativeAmount` is the zero value. - pub fn is_zero(&self) -> bool { - self == &NonNegativeAmount::ZERO - } - - /// Returns whether or not this `NonNegativeAmount` is positive. - pub fn is_positive(&self) -> bool { - self > &NonNegativeAmount::ZERO - } -} - -impl From for Amount { - fn from(n: NonNegativeAmount) -> Self { - n.0 - } -} - -impl From<&NonNegativeAmount> for Amount { - fn from(n: &NonNegativeAmount) -> Self { - n.0 - } -} - -impl From for u64 { - fn from(n: NonNegativeAmount) -> Self { - n.0.try_into().unwrap() - } -} - -impl From for sapling::value::NoteValue { - fn from(n: NonNegativeAmount) -> Self { - sapling::value::NoteValue::from_raw(n.into()) - } -} - -impl TryFrom for NonNegativeAmount { - type Error = (); - - fn try_from(value: sapling::value::NoteValue) -> Result { - Self::from_u64(value.inner()) - } -} - -impl From for orchard::NoteValue { - fn from(n: NonNegativeAmount) -> Self { - orchard::NoteValue::from_raw(n.into()) - } -} - -impl TryFrom for NonNegativeAmount { - type Error = (); - - fn try_from(value: orchard::NoteValue) -> Result { - Self::from_u64(value.inner()) - } -} - -impl TryFrom for NonNegativeAmount { - type Error = (); - - fn try_from(value: Amount) -> Result { - if value.is_negative() { - Err(()) - } else { - Ok(NonNegativeAmount(value)) - } - } -} - -impl Add for NonNegativeAmount { - type Output = Option; - - fn add(self, rhs: NonNegativeAmount) -> Option { - (self.0 + rhs.0).map(NonNegativeAmount) - } -} - -impl Add for Option { - type Output = Self; - - fn add(self, rhs: NonNegativeAmount) -> Option { - self.and_then(|lhs| lhs + rhs) - } -} - -impl Sub for NonNegativeAmount { - type Output = Option; - - fn sub(self, rhs: NonNegativeAmount) -> Option { - (self.0 - rhs.0).and_then(|amt| NonNegativeAmount::try_from(amt).ok()) - } -} - -impl Sub for Option { - type Output = Self; - - fn sub(self, rhs: NonNegativeAmount) -> Option { - self.and_then(|lhs| lhs - rhs) - } -} - -impl Mul for NonNegativeAmount { - type Output = Option; - - fn mul(self, rhs: usize) -> Option { - (self.0 * rhs).and_then(|v| NonNegativeAmount::try_from(v).ok()) - } -} - -impl Sum for Option { - fn sum>(iter: I) -> Self { - iter.fold(Some(NonNegativeAmount::ZERO), |acc, a| acc? + a) - } -} - -impl<'a> Sum<&'a NonNegativeAmount> for Option { - fn sum>(iter: I) -> Self { - iter.fold(Some(NonNegativeAmount::ZERO), |acc, a| acc? + *a) - } -} - -/// A type for balance violations in amount addition and subtraction -/// (overflow and underflow of allowed ranges) -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum BalanceError { - Overflow, - Underflow, -} - -impl error::Error for BalanceError {} - -impl std::fmt::Display for BalanceError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match &self { - BalanceError::Overflow => { - write!( - f, - "Amount addition resulted in a value outside the valid range." - ) - } - BalanceError::Underflow => write!( - f, - "Amount subtraction resulted in a value outside the valid range." - ), - } - } -} - -impl From for BalanceError { - fn from(_value: Infallible) -> Self { - unreachable!() - } -} - -#[cfg(any(test, feature = "test-dependencies"))] -pub mod testing { - use proptest::prelude::prop_compose; - - use super::{Amount, NonNegativeAmount, MAX_MONEY}; - - prop_compose! { - pub fn arb_amount()(amt in -MAX_MONEY..MAX_MONEY) -> Amount { - Amount::from_i64(amt).unwrap() - } - } - - prop_compose! { - pub fn arb_nonnegative_amount()(amt in 0i64..MAX_MONEY) -> NonNegativeAmount { - NonNegativeAmount::from_u64(amt as u64).unwrap() - } - } - - prop_compose! { - pub fn arb_positive_amount()(amt in 1i64..MAX_MONEY) -> Amount { - Amount::from_i64(amt).unwrap() - } - } -} - -#[cfg(test)] -mod tests { - use super::{Amount, MAX_MONEY}; - - #[test] - fn amount_in_range() { - let zero = b"\x00\x00\x00\x00\x00\x00\x00\x00"; - assert_eq!(Amount::from_u64_le_bytes(*zero).unwrap(), Amount(0)); - assert_eq!( - Amount::from_nonnegative_i64_le_bytes(*zero).unwrap(), - Amount(0) - ); - assert_eq!(Amount::from_i64_le_bytes(*zero).unwrap(), Amount(0)); - - let neg_one = b"\xff\xff\xff\xff\xff\xff\xff\xff"; - assert!(Amount::from_u64_le_bytes(*neg_one).is_err()); - assert!(Amount::from_nonnegative_i64_le_bytes(*neg_one).is_err()); - assert_eq!(Amount::from_i64_le_bytes(*neg_one).unwrap(), Amount(-1)); - - let max_money = b"\x00\x40\x07\x5a\xf0\x75\x07\x00"; - assert_eq!( - Amount::from_u64_le_bytes(*max_money).unwrap(), - Amount(MAX_MONEY) - ); - assert_eq!( - Amount::from_nonnegative_i64_le_bytes(*max_money).unwrap(), - Amount(MAX_MONEY) - ); - assert_eq!( - Amount::from_i64_le_bytes(*max_money).unwrap(), - Amount(MAX_MONEY) - ); - - let max_money_p1 = b"\x01\x40\x07\x5a\xf0\x75\x07\x00"; - assert!(Amount::from_u64_le_bytes(*max_money_p1).is_err()); - assert!(Amount::from_nonnegative_i64_le_bytes(*max_money_p1).is_err()); - assert!(Amount::from_i64_le_bytes(*max_money_p1).is_err()); - - let neg_max_money = b"\x00\xc0\xf8\xa5\x0f\x8a\xf8\xff"; - assert!(Amount::from_u64_le_bytes(*neg_max_money).is_err()); - assert!(Amount::from_nonnegative_i64_le_bytes(*neg_max_money).is_err()); - assert_eq!( - Amount::from_i64_le_bytes(*neg_max_money).unwrap(), - Amount(-MAX_MONEY) - ); - - let neg_max_money_m1 = b"\xff\xbf\xf8\xa5\x0f\x8a\xf8\xff"; - assert!(Amount::from_u64_le_bytes(*neg_max_money_m1).is_err()); - assert!(Amount::from_nonnegative_i64_le_bytes(*neg_max_money_m1).is_err()); - assert!(Amount::from_i64_le_bytes(*neg_max_money_m1).is_err()); - } - - #[test] - fn add_overflow() { - let v = Amount(MAX_MONEY); - assert_eq!(v + Amount(1), None) - } - - #[test] - #[should_panic] - fn add_assign_panics_on_overflow() { - let mut a = Amount(MAX_MONEY); - a += Amount(1); - } - - #[test] - fn sub_underflow() { - let v = Amount(-MAX_MONEY); - assert_eq!(v - Amount(1), None) - } - - #[test] - #[should_panic] - fn sub_assign_panics_on_underflow() { - let mut a = Amount(-MAX_MONEY); - a -= Amount(1); - } -} diff --git a/zcash_primitives/src/transaction/components/sapling.rs b/zcash_primitives/src/transaction/components/sapling.rs index 64f4764012..62e493865d 100644 --- a/zcash_primitives/src/transaction/components/sapling.rs +++ b/zcash_primitives/src/transaction/components/sapling.rs @@ -1,5 +1,7 @@ use ff::PrimeField; use redjubjub::SpendAuth; +use sapling::note_encryption::Zip212Enforcement; +use zcash_protocol::consensus::{BlockHeight, NetworkUpgrade, Parameters, ZIP212_GRACE_PERIOD}; use std::io::{self, Read, Write}; @@ -21,6 +23,22 @@ use crate::{ use super::{Amount, GROTH_PROOF_SIZE}; +/// Returns the enforcement policy for ZIP 212 at the given height. +pub fn zip212_enforcement(params: &impl Parameters, height: BlockHeight) -> Zip212Enforcement { + if params.is_nu_active(NetworkUpgrade::Canopy, height) { + let grace_period_end_height = + params.activation_height(NetworkUpgrade::Canopy).unwrap() + ZIP212_GRACE_PERIOD; + + if height < grace_period_end_height { + Zip212Enforcement::GracePeriod + } else { + Zip212Enforcement::On + } + } else { + Zip212Enforcement::Off + } +} + /// A map from one bundle authorization to another. /// /// For use with [`TransactionData::map_authorization`].