From c754363c0798867c550827b326a5571b5d1a1226 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sun, 7 Mar 2021 18:18:16 +0000 Subject: [PATCH 01/18] zcash_address: Initial empty library crate --- Cargo.toml | 1 + components/zcash_address/Cargo.toml | 14 ++ components/zcash_address/LICENSE-APACHE | 202 ++++++++++++++++++++++++ components/zcash_address/LICENSE-MIT | 21 +++ components/zcash_address/README.md | 20 +++ components/zcash_address/src/lib.rs | 7 + 6 files changed, 265 insertions(+) create mode 100644 components/zcash_address/Cargo.toml create mode 100644 components/zcash_address/LICENSE-APACHE create mode 100644 components/zcash_address/LICENSE-MIT create mode 100644 components/zcash_address/README.md create mode 100644 components/zcash_address/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 846f8c7a7b..21f33296ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "components/equihash", + "components/zcash_address", "components/zcash_note_encryption", "zcash_client_backend", "zcash_client_sqlite", diff --git a/components/zcash_address/Cargo.toml b/components/zcash_address/Cargo.toml new file mode 100644 index 0000000000..e967c69ccf --- /dev/null +++ b/components/zcash_address/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "zcash_address" +description = "Zcash address parsing and serialization" +version = "0.0.0" +authors = [ + "Jack Grigg ", +] +homepage = "https://github.com/zcash/librustzcash" +repository = "https://github.com/zcash/librustzcash" +readme = "README.md" +license = "MIT OR Apache-2.0" +edition = "2018" + +[dependencies] diff --git a/components/zcash_address/LICENSE-APACHE b/components/zcash_address/LICENSE-APACHE new file mode 100644 index 0000000000..1e5006dc14 --- /dev/null +++ b/components/zcash_address/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_address/LICENSE-MIT b/components/zcash_address/LICENSE-MIT new file mode 100644 index 0000000000..9500c140cc --- /dev/null +++ b/components/zcash_address/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 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_address/README.md b/components/zcash_address/README.md new file mode 100644 index 0000000000..46867e3e91 --- /dev/null +++ b/components/zcash_address/README.md @@ -0,0 +1,20 @@ +# zcash_address + +TBD + +## 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/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs new file mode 100644 index 0000000000..31e1bb209f --- /dev/null +++ b/components/zcash_address/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} From ec77175d2b05763190e1d5fb1d9788f714e7a29b Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 8 Mar 2021 03:25:59 +0000 Subject: [PATCH 02/18] zcash_address: Add address-encoding support This provides round-trip encoding for Zcash addresses. --- components/zcash_address/Cargo.toml | 2 + components/zcash_address/src/encoding.rs | 314 +++++++++++++++++++ components/zcash_address/src/kind.rs | 6 + components/zcash_address/src/kind/orchard.rs | 18 ++ components/zcash_address/src/kind/p2pkh.rs | 7 + components/zcash_address/src/kind/p2sh.rs | 7 + components/zcash_address/src/kind/sapling.rs | 22 ++ components/zcash_address/src/kind/sprout.rs | 15 + components/zcash_address/src/lib.rs | 40 ++- 9 files changed, 425 insertions(+), 6 deletions(-) create mode 100644 components/zcash_address/src/encoding.rs create mode 100644 components/zcash_address/src/kind.rs create mode 100644 components/zcash_address/src/kind/orchard.rs create mode 100644 components/zcash_address/src/kind/p2pkh.rs create mode 100644 components/zcash_address/src/kind/p2sh.rs create mode 100644 components/zcash_address/src/kind/sapling.rs create mode 100644 components/zcash_address/src/kind/sprout.rs diff --git a/components/zcash_address/Cargo.toml b/components/zcash_address/Cargo.toml index e967c69ccf..bac4703acc 100644 --- a/components/zcash_address/Cargo.toml +++ b/components/zcash_address/Cargo.toml @@ -12,3 +12,5 @@ license = "MIT OR Apache-2.0" edition = "2018" [dependencies] +bech32 = "0.8" +bs58 = { version = "0.4", features = ["check"] } diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs new file mode 100644 index 0000000000..5e57c3e2ce --- /dev/null +++ b/components/zcash_address/src/encoding.rs @@ -0,0 +1,314 @@ +use std::{convert::TryInto, error::Error, fmt, str::FromStr}; + +use bech32::{self, FromBase32, ToBase32, Variant}; + +use crate::{kind::*, AddressKind, Network, ZcashAddress}; + +/// An error while attempting to parse a string as a Zcash address. +#[derive(Debug, PartialEq)] +pub enum ParseError { + /// The string is an invalid encoding. + InvalidEncoding, + /// The string might be an unknown Zcash address from the future. + MaybeZcash, + /// The string is not a Zcash address. + NotZcash, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParseError::InvalidEncoding => write!(f, "Invalid encoding"), + ParseError::MaybeZcash => write!( + f, + "This might be a Zcash address from the future that we don't know about" + ), + ParseError::NotZcash => write!(f, "Not a Zcash address"), + } + } +} + +impl Error for ParseError {} + +impl FromStr for ZcashAddress { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + // Remove leading and trailing whitespace, to handle copy-paste errors. + let s = s.trim(); + + // Most Zcash addresses use Bech32, so try that first. + match bech32::decode(s) { + // Zcash addresses only use the original Bech32 variant, since the data + // corresponding to a particular HRP always has a fixed length. + Ok((_, _, Variant::Bech32m)) => return Err(ParseError::NotZcash), + Ok((hrp, data, Variant::Bech32)) => { + // If we reached this point, the encoding is supposed to be valid Bech32. + let data = + Vec::::from_base32(&data).map_err(|_| ParseError::InvalidEncoding)?; + + let net = match hrp.as_str() { + sapling::MAINNET | orchard::MAINNET => Network::Main, + sapling::TESTNET | orchard::TESTNET => Network::Test, + sapling::REGTEST | orchard::REGTEST => Network::Regtest, + _ => { + // Use some heuristics to try and guess whether this might be a Zcash + // address from the future: + // - Zcash HRPs always start with a 'z'. + // - Zcash shielded addresses with diversification have data of + // length 43, but if we added the simple form of detection keys + // the data would have length 75. + return Err( + if hrp.starts_with('z') && (data.len() == 43 || data.len() == 75) { + ParseError::MaybeZcash + } else { + ParseError::NotZcash + }, + ); + } + }; + + return match hrp.as_str() { + sapling::MAINNET | sapling::TESTNET | sapling::REGTEST => { + data[..].try_into().map(AddressKind::Sapling) + } + orchard::MAINNET | orchard::TESTNET | orchard::REGTEST => { + data[..].try_into().map(AddressKind::Orchard) + } + _ => unreachable!(), + } + .map_err(|_| ParseError::InvalidEncoding) + .map(|kind| ZcashAddress { net, kind }); + } + Err(_) => (), + } + + // 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), + }; + + return match decoded[..2].try_into().unwrap() { + sprout::MAINNET | sprout::TESTNET => { + decoded[2..].try_into().map(AddressKind::Sprout) + } + 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 }); + }; + + // If it's not valid Bech32 or Base58Check, it's not a Zcash address. + Err(ParseError::NotZcash) + } +} + +fn encode_bech32(hrp: &str, data: &[u8]) -> String { + bech32::encode(hrp, data.to_base32(), Variant::Bech32).expect("hrp is invalid") +} + +fn encode_b58(prefix: [u8; 2], data: &[u8]) -> String { + let mut decoded = Vec::with_capacity(2 + data.len()); + decoded.extend_from_slice(&prefix); + decoded.extend_from_slice(data); + bs58::encode(decoded).with_check().into_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( + if let Network::Main = self.net { + sprout::MAINNET + } else { + 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::Orchard(data) => encode_bech32( + match self.net { + Network::Main => orchard::MAINNET, + Network::Test => orchard::TESTNET, + Network::Regtest => orchard::REGTEST, + }, + &data, + ), + AddressKind::P2pkh(data) => encode_b58( + if let Network::Main = self.net { + p2pkh::MAINNET + } else { + p2pkh::TESTNET + }, + &data, + ), + AddressKind::P2sh(data) => encode_b58( + if let Network::Main = self.net { + p2sh::MAINNET + } else { + p2sh::TESTNET + }, + &data, + ), + }; + write!(f, "{}", encoded) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn encoding(encoded: &str, decoded: ZcashAddress) { + assert_eq!(decoded.to_string(), encoded); + assert_eq!(encoded.parse(), Ok(decoded)); + } + + #[test] + fn sprout() { + encoding( + "zc8E5gYid86n4bo2Usdq1cpr7PpfoJGzttwBHEEgGhGkLUg7SPPVFNB2AkRFXZ7usfphup5426dt1buMmY3fkYeRrQGLa8y", + ZcashAddress { net: Network::Main, kind: AddressKind::Sprout([0; 64]) }, + ); + encoding( + "ztJ1EWLKcGwF2S4NA17pAJVdco8Sdkz4AQPxt1cLTEfNuyNswJJc2BbBqYrsRZsp31xbVZwhF7c7a2L9jsF3p3ZwRWpqqyS", + ZcashAddress { net: Network::Test, kind: AddressKind::Sprout([0; 64]) }, + ); + } + + #[test] + fn sapling() { + encoding( + "zs1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpq6d8g", + ZcashAddress { + net: Network::Main, + kind: AddressKind::Sapling([0; 43]), + }, + ); + encoding( + "ztestsapling1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqfhgwqu", + ZcashAddress { + net: Network::Test, + kind: AddressKind::Sapling([0; 43]), + }, + ); + encoding( + "zregtestsapling1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqknpr3m", + ZcashAddress { + net: Network::Regtest, + kind: AddressKind::Sapling([0; 43]), + }, + ); + } + + #[test] + fn orchard() { + encoding( + "zo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq58lk79", + ZcashAddress { + net: Network::Main, + kind: AddressKind::Orchard([0; 43]), + }, + ); + encoding( + "ztestorchard1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqcrmt3p", + ZcashAddress { + net: Network::Test, + kind: AddressKind::Orchard([0; 43]), + }, + ); + encoding( + "zregtestorchard1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq88jxqx", + ZcashAddress { + net: Network::Regtest, + kind: AddressKind::Orchard([0; 43]), + }, + ); + } + + #[test] + fn maybe_zcash() { + assert_eq!( + "zmaybe1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqql7xs38" + .parse::(), + Err(ParseError::MaybeZcash), + ); + assert_eq!( + "zpossibly1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq559klt" + .parse::(), + Err(ParseError::MaybeZcash), + ); + assert_eq!( + "nope1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg8f5j9" + .parse::(), + Err(ParseError::NotZcash), + ); + } + + #[test] + fn transparent() { + encoding( + "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs", + ZcashAddress { + net: Network::Main, + kind: AddressKind::P2pkh([0; 20]), + }, + ); + encoding( + "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma", + ZcashAddress { + net: Network::Test, + kind: AddressKind::P2pkh([0; 20]), + }, + ); + encoding( + "t3JZcvsuaXE6ygokL4XUiZSTrQBUoPYFnXJ", + ZcashAddress { + net: Network::Main, + kind: AddressKind::P2sh([0; 20]), + }, + ); + encoding( + "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2", + ZcashAddress { + net: Network::Test, + kind: AddressKind::P2sh([0; 20]), + }, + ); + } + + #[test] + fn whitespace() { + assert_eq!( + " t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse(), + Ok(ZcashAddress { + net: Network::Main, + kind: AddressKind::P2pkh([0; 20]) + }), + ); + assert_eq!( + "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs ".parse(), + Ok(ZcashAddress { + net: Network::Main, + kind: AddressKind::P2pkh([0; 20]) + }), + ); + assert_eq!( + "something t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse::(), + Err(ParseError::NotZcash), + ); + } +} diff --git a/components/zcash_address/src/kind.rs b/components/zcash_address/src/kind.rs new file mode 100644 index 0000000000..647ceec594 --- /dev/null +++ b/components/zcash_address/src/kind.rs @@ -0,0 +1,6 @@ +pub(crate) mod orchard; +pub(crate) mod sapling; +pub(crate) mod sprout; + +pub(crate) mod p2pkh; +pub(crate) mod p2sh; diff --git a/components/zcash_address/src/kind/orchard.rs b/components/zcash_address/src/kind/orchard.rs new file mode 100644 index 0000000000..3641fe17ae --- /dev/null +++ b/components/zcash_address/src/kind/orchard.rs @@ -0,0 +1,18 @@ +/// The HRP for a Bech32-encoded mainnet Orchard address. +/// +/// Defined in the [Zcash Protocol Specification section 5.6.4.1][orchardpaymentaddrencoding]. +/// +/// [orchardpaymentaddrencoding]: https://zips.z.cash/protocol/nu5.pdf#orchardpaymentaddrencoding +pub(crate) const MAINNET: &str = "zo"; + +/// The HRP for a Bech32-encoded testnet Orchard address. +/// +/// Defined in the [Zcash Protocol Specification section 5.6.4.1][orchardpaymentaddrencoding]. +/// +/// [orchardpaymentaddrencoding]: https://zips.z.cash/protocol/nu5.pdf#orchardpaymentaddrencoding +pub(crate) const TESTNET: &str = "ztestorchard"; + +/// The HRP for a Bech32-encoded regtest Orchard address. +pub(crate) const REGTEST: &str = "zregtestorchard"; + +pub(crate) type Data = [u8; 43]; diff --git a/components/zcash_address/src/kind/p2pkh.rs b/components/zcash_address/src/kind/p2pkh.rs new file mode 100644 index 0000000000..0120e2c39f --- /dev/null +++ b/components/zcash_address/src/kind/p2pkh.rs @@ -0,0 +1,7 @@ +/// 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]; + +pub(crate) type Data = [u8; 20]; diff --git a/components/zcash_address/src/kind/p2sh.rs b/components/zcash_address/src/kind/p2sh.rs new file mode 100644 index 0000000000..5059513182 --- /dev/null +++ b/components/zcash_address/src/kind/p2sh.rs @@ -0,0 +1,7 @@ +/// 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]; + +pub(crate) type Data = [u8; 20]; diff --git a/components/zcash_address/src/kind/sapling.rs b/components/zcash_address/src/kind/sapling.rs new file mode 100644 index 0000000000..2cbf914d61 --- /dev/null +++ b/components/zcash_address/src/kind/sapling.rs @@ -0,0 +1,22 @@ +/// 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"; + +pub(crate) type Data = [u8; 43]; diff --git a/components/zcash_address/src/kind/sprout.rs b/components/zcash_address/src/kind/sprout.rs new file mode 100644 index 0000000000..fedb79b416 --- /dev/null +++ b/components/zcash_address/src/kind/sprout.rs @@ -0,0 +1,15 @@ +/// 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][]. +/// +/// []: https://zips.z.cash/protocol/protocol.pdf#sproutpaymentaddrencoding +pub(crate) const TESTNET: [u8; 2] = [0x16, 0xb6]; + +pub(crate) type Data = [u8; 64]; diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index 31e1bb209f..3a01029e72 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -1,7 +1,35 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } +mod encoding; +mod kind; + +pub use encoding::ParseError; + +/// A Zcash address. +#[derive(Debug, PartialEq)] +pub struct ZcashAddress { + net: Network, + kind: AddressKind, +} + +/// The Zcash network for which an address is encoded. +#[derive(Debug, PartialEq)] +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(Debug, PartialEq)] +enum AddressKind { + Sprout(kind::sprout::Data), + Sapling(kind::sapling::Data), + Orchard(kind::orchard::Data), + P2pkh(kind::p2pkh::Data), + P2sh(kind::p2sh::Data), } From a366460157f600c81814eba0c184bd4cc6bce764 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 8 Mar 2021 04:46:19 +0000 Subject: [PATCH 03/18] zcash_address: ZcashAddress::convert() -> T: FromAddress This enables easy conversion of an encoded Zcash address to a target type, with automatic handling of Zcash address types that are not supported by the target. --- components/zcash_address/src/convert.rs | 86 +++++++++++++++++++++++++ components/zcash_address/src/lib.rs | 4 +- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 components/zcash_address/src/convert.rs diff --git a/components/zcash_address/src/convert.rs b/components/zcash_address/src/convert.rs new file mode 100644 index 0000000000..50a500ad4a --- /dev/null +++ b/components/zcash_address/src/convert.rs @@ -0,0 +1,86 @@ +use std::{error::Error, fmt}; + +use crate::{kind::*, AddressKind, Network, ZcashAddress}; + +/// An address type is not supported for conversion. +#[derive(Debug)] +pub struct UnsupportedAddress(&'static str); + +impl fmt::Display for UnsupportedAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Zcash {} addresses are not supported", self.0) + } +} + +impl Error for UnsupportedAddress {} + +impl ZcashAddress { + pub fn convert(self) -> Result { + match self.kind { + AddressKind::Sprout(data) => T::from_sprout(self.net, data), + AddressKind::Sapling(data) => T::from_sapling(self.net, data), + AddressKind::Orchard(data) => T::from_orchard(self.net, data), + AddressKind::P2pkh(data) => T::from_transparent_p2pkh(self.net, data), + AddressKind::P2sh(data) => T::from_transparent_p2sh(self.net, data), + } + } +} + +/// A helper trait for converting a [`ZcashAddress`] into another type. +/// +/// # Examples +/// +/// ``` +/// use zcash_address::{FromAddress, Network, UnsupportedAddress, ZcashAddress}; +/// +/// #[derive(Debug)] +/// struct MySapling([u8; 43]); +/// +/// // Implement the FromAddress trait, overriding whichever conversion methods match your +/// // requirements for the resulting type. +/// impl FromAddress for MySapling { +/// fn from_sapling(net: Network, data: [u8; 43]) -> Result { +/// Ok(MySapling(data)) +/// } +/// } +/// +/// // For a supported address type, the conversion works. +/// let addr: ZcashAddress = +/// "zs1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpq6d8g" +/// .parse() +/// .unwrap(); +/// assert!(addr.convert::().is_ok()); +/// +/// // For an unsupported address type, we get an error. +/// let addr: ZcashAddress = "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs".parse().unwrap(); +/// assert_eq!( +/// addr.convert::().unwrap_err().to_string(), +/// "Zcash transparent P2PKH addresses are not supported", +/// ); +/// ``` +pub trait FromAddress: Sized { + fn from_sprout(net: Network, data: sprout::Data) -> Result { + let _ = (net, data); + Err(UnsupportedAddress("Sprout")) + } + + fn from_sapling(net: Network, data: sapling::Data) -> Result { + let _ = (net, data); + Err(UnsupportedAddress("Sapling")) + } + + fn from_orchard(net: Network, data: orchard::Data) -> Result { + let _ = (net, data); + Err(UnsupportedAddress("Orchard")) + } + + fn from_transparent_p2pkh(net: Network, data: p2pkh::Data) -> Result { + let _ = (net, data); + Err(UnsupportedAddress("transparent P2PKH")) + } + + fn from_transparent_p2sh(net: Network, data: p2sh::Data) -> Result { + let _ = (net, data); + Err(UnsupportedAddress("transparent P2SH")) + } +} diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index 3a01029e72..76139aca12 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -1,6 +1,8 @@ +mod convert; mod encoding; mod kind; +pub use convert::{FromAddress, UnsupportedAddress}; pub use encoding::ParseError; /// A Zcash address. @@ -12,7 +14,7 @@ pub struct ZcashAddress { /// The Zcash network for which an address is encoded. #[derive(Debug, PartialEq)] -enum Network { +pub enum Network { /// Zcash Mainnet. Main, /// Zcash Testnet. From f7b105817157d56731c41f4838c8cea0f6c03c16 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 9 Mar 2021 13:51:59 +1300 Subject: [PATCH 04/18] zcash_address: Extend MaybeZcash heuristics to 64-byte addresses --- components/zcash_address/src/encoding.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index 5e57c3e2ce..d266b1ced3 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -57,9 +57,12 @@ impl FromStr for ZcashAddress { // - Zcash HRPs always start with a 'z'. // - Zcash shielded addresses with diversification have data of // length 43, but if we added the simple form of detection keys - // the data would have length 75. + // the data would have length 75. Alternatively if we switch from a + // 11-byte diversifier to two field elements, that would be 64 bytes. return Err( - if hrp.starts_with('z') && (data.len() == 43 || data.len() == 75) { + if hrp.starts_with('z') + && (data.len() == 43 || data.len() == 64 || data.len() == 75) + { ParseError::MaybeZcash } else { ParseError::NotZcash From c7fcee27a229ea55ce20738c606c719628d791bf Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sat, 13 Mar 2021 09:11:03 +1300 Subject: [PATCH 05/18] zcash_address: Add ZcashAddress::try_from_encoded method This places parsing documentation front and centre, while also making it clear that `str::parse` is the anticipated main entry point. --- components/zcash_address/src/encoding.rs | 1 + components/zcash_address/src/lib.rs | 36 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index d266b1ced3..2a898d335b 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -33,6 +33,7 @@ impl Error for ParseError {} impl FromStr for ZcashAddress { type Err = ParseError; + /// Attempts to parse the given string as a Zcash address. fn from_str(s: &str) -> Result { // Remove leading and trailing whitespace, to handle copy-paste errors. let s = s.trim(); diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index 76139aca12..ea52e678cc 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -35,3 +35,39 @@ enum AddressKind { P2pkh(kind::p2pkh::Data), P2sh(kind::p2sh::Data), } + +impl ZcashAddress { + /// Attempts to parse the given string as a Zcash address. + /// + /// This simply calls [`s.parse()`], leveraging the [`FromStr` implementation]. + /// + /// [`s.parse()`]: std::primitive::str::parse + /// [`FromStr` implementation]: ZcashAddress#impl-FromStr + /// + /// # Errors + /// + /// In most cases, [`ParseError::NotZcash`] will be returned on failure. The two + /// exceptions are: + /// + /// - If the parser can detect that the string _must_ contain an address encoding used + /// by Zcash, [`ParseError::InvalidEncoding`] will be returned if any subsequent + /// part of that encoding is invalid. + /// + /// - [`ParseError::MaybeZcash`] will be returned if the string is Bech32-encoded data + /// that satisfies some heuristics for probable future Zcash address formats (such + /// as beginning with a `z`). This can either be treated as an indication that this + /// library dependency should be updated, or mapped to [`ParseError::NotZcash`]. + /// + /// # Examples + /// + /// ``` + /// use zcash_address::ZcashAddress; + /// + /// let encoded = "zs1z7rejlpsa98s2rrrfkwmaxu53e4ue0ulcrw0h4x5g8jl04tak0d3mm47vdtahatqrlkngh9sly"; + /// let addr = ZcashAddress::try_from_encoded(&encoded); + /// assert_eq!(encoded.parse(), addr); + /// ``` + pub fn try_from_encoded(s: &str) -> Result { + s.parse() + } +} From b9f704955ab210241b7338669c3859f024a35de2 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sat, 13 Mar 2021 09:12:52 +1300 Subject: [PATCH 06/18] zcash_address: Move ZcashAddress::convert into root Using two separate `impl ZcashAddress` blocks resulted in separate blocks in the documentation, which is unnecessary. --- components/zcash_address/src/convert.rs | 16 +++------------- components/zcash_address/src/lib.rs | 10 ++++++++++ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/components/zcash_address/src/convert.rs b/components/zcash_address/src/convert.rs index 50a500ad4a..ec6685a183 100644 --- a/components/zcash_address/src/convert.rs +++ b/components/zcash_address/src/convert.rs @@ -1,6 +1,6 @@ use std::{error::Error, fmt}; -use crate::{kind::*, AddressKind, Network, ZcashAddress}; +use crate::{kind::*, Network}; /// An address type is not supported for conversion. #[derive(Debug)] @@ -14,20 +14,10 @@ impl fmt::Display for UnsupportedAddress { impl Error for UnsupportedAddress {} -impl ZcashAddress { - pub fn convert(self) -> Result { - match self.kind { - AddressKind::Sprout(data) => T::from_sprout(self.net, data), - AddressKind::Sapling(data) => T::from_sapling(self.net, data), - AddressKind::Orchard(data) => T::from_orchard(self.net, data), - AddressKind::P2pkh(data) => T::from_transparent_p2pkh(self.net, data), - AddressKind::P2sh(data) => T::from_transparent_p2sh(self.net, data), - } - } -} - /// A helper trait for converting a [`ZcashAddress`] into another type. /// +/// [`ZcashAddress`]: crate::ZcashAddress +/// /// # Examples /// /// ``` diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index ea52e678cc..eca9832471 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -70,4 +70,14 @@ impl ZcashAddress { pub fn try_from_encoded(s: &str) -> Result { s.parse() } + + pub fn convert(self) -> Result { + match self.kind { + AddressKind::Sprout(data) => T::from_sprout(self.net, data), + AddressKind::Sapling(data) => T::from_sapling(self.net, data), + AddressKind::Orchard(data) => T::from_orchard(self.net, data), + AddressKind::P2pkh(data) => T::from_transparent_p2pkh(self.net, data), + AddressKind::P2sh(data) => T::from_transparent_p2sh(self.net, data), + } + } } From ff07eeaabb68a2a270e7e502e6e4ffbf07d08a03 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sat, 13 Mar 2021 10:42:56 +1300 Subject: [PATCH 07/18] zcash_address: Document ZcashAddress::convert --- components/zcash_address/src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index eca9832471..94881207a0 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -71,6 +71,18 @@ impl ZcashAddress { s.parse() } + /// Converts this address into another type. + /// + /// `convert` can convert into any type that implements the [`FromAddress`] trait. + /// This enables `ZcashAddress` to be used as a common parsing and serialization + /// interface for Zcash addresses, while delegating operations on those addresses + /// (such as constructing transactions) to downstream crates. + /// + /// If you want to get the encoded string for this address, use the [`Display`] + /// implementation instead via [`address.to_string()`]. + /// + /// [`Display`]: std::fmt::Display + /// [`address.to_string()`]: std::string::ToString pub fn convert(self) -> Result { match self.kind { AddressKind::Sprout(data) => T::from_sprout(self.net, data), From ce8797e4b1dd1039709cdf7be998d4e708f83231 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sat, 13 Mar 2021 10:43:20 +1300 Subject: [PATCH 08/18] zcash_address: impl {Clone, Copy, Eq, Hash} for ZcashAddress --- components/zcash_address/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index 94881207a0..af5dea0683 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -6,14 +6,14 @@ pub use convert::{FromAddress, UnsupportedAddress}; pub use encoding::ParseError; /// A Zcash address. -#[derive(Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ZcashAddress { net: Network, kind: AddressKind, } /// The Zcash network for which an address is encoded. -#[derive(Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Network { /// Zcash Mainnet. Main, @@ -27,7 +27,7 @@ pub enum Network { } /// Known kinds of Zcash addresses. -#[derive(Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] enum AddressKind { Sprout(kind::sprout::Data), Sapling(kind::sapling::Data), From 9f7398cd051ed32f8d40ae57bfd290d1c984704f Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sat, 13 Mar 2021 11:24:03 +1300 Subject: [PATCH 09/18] zcash_address: Add convert::ToAddress helper trait --- components/zcash_address/src/convert.rs | 101 +++++++++++++++++++++++- components/zcash_address/src/lib.rs | 2 +- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/components/zcash_address/src/convert.rs b/components/zcash_address/src/convert.rs index ec6685a183..66e4664638 100644 --- a/components/zcash_address/src/convert.rs +++ b/components/zcash_address/src/convert.rs @@ -1,6 +1,6 @@ use std::{error::Error, fmt}; -use crate::{kind::*, Network}; +use crate::{kind::*, AddressKind, Network, ZcashAddress}; /// An address type is not supported for conversion. #[derive(Debug)] @@ -74,3 +74,102 @@ pub trait FromAddress: Sized { Err(UnsupportedAddress("transparent P2SH")) } } + +/// A helper trait for converting another type into a [`ZcashAddress`]. +/// +/// This trait is sealed and cannot be implemented for types outside this crate. Its +/// purpose is to move these conversion functions out of the main `ZcashAddress` API +/// documentation, as they are only required when creating addresses (rather than when +/// parsing addresses, which is a more common occurrence). +/// +/// [`ZcashAddress`]: crate::ZcashAddress +/// +/// # Examples +/// +/// ``` +/// use zcash_address::{ToAddress, Network, ZcashAddress}; +/// +/// #[derive(Debug)] +/// struct MySapling([u8; 43]); +/// +/// impl MySapling { +/// /// Encodes this Sapling address for the given network. +/// fn encode(&self, net: Network) -> ZcashAddress { +/// ZcashAddress::from_sapling(net, self.0) +/// } +/// } +/// +/// let addr = MySapling([0; 43]); +/// let encoded = addr.encode(Network::Main); +/// assert_eq!( +/// encoded.to_string(), +/// "zs1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpq6d8g", +/// ); +/// ``` +pub trait ToAddress: private::Sealed { + fn from_sprout(net: Network, data: sprout::Data) -> Self; + + fn from_sapling(net: Network, data: sapling::Data) -> Self; + + fn from_orchard(net: Network, data: orchard::Data) -> Self; + + fn from_transparent_p2pkh(net: Network, data: p2pkh::Data) -> Self; + + fn from_transparent_p2sh(net: Network, data: p2sh::Data) -> Self; +} + +impl ToAddress for ZcashAddress { + fn from_sprout(net: Network, data: sprout::Data) -> Self { + ZcashAddress { + net: if let Network::Regtest = net { + Network::Test + } else { + net + }, + kind: AddressKind::Sprout(data), + } + } + + fn from_sapling(net: Network, data: sapling::Data) -> Self { + ZcashAddress { + net, + kind: AddressKind::Sapling(data), + } + } + + fn from_orchard(net: Network, data: orchard::Data) -> Self { + ZcashAddress { + net, + kind: AddressKind::Orchard(data), + } + } + + fn from_transparent_p2pkh(net: Network, data: p2pkh::Data) -> Self { + ZcashAddress { + net: if let Network::Regtest = net { + Network::Test + } else { + net + }, + kind: AddressKind::P2pkh(data), + } + } + + fn from_transparent_p2sh(net: Network, data: p2sh::Data) -> Self { + ZcashAddress { + net: if let Network::Regtest = net { + Network::Test + } else { + net + }, + kind: AddressKind::P2sh(data), + } + } +} + +mod private { + use crate::ZcashAddress; + + pub trait Sealed {} + impl Sealed for ZcashAddress {} +} diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index af5dea0683..cd239779de 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -2,7 +2,7 @@ mod convert; mod encoding; mod kind; -pub use convert::{FromAddress, UnsupportedAddress}; +pub use convert::{FromAddress, ToAddress, UnsupportedAddress}; pub use encoding::ParseError; /// A Zcash address. From ae2b8bfd6d3d8b2dd096bd5a4052df38e1a7cf01 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 20 May 2021 15:12:33 +0100 Subject: [PATCH 10/18] zcash_address: Replace Orchard address encodings with Unified Addresses This commit removes the now-undefined Orchard encoding logic, and adds the general Bech32m encoding/decoding logic for Unified Addresses. The internal data format of Unified Addresses is not correct in this commit. --- components/zcash_address/src/convert.rs | 10 +- components/zcash_address/src/encoding.rs | 109 ++++++++----------- components/zcash_address/src/kind.rs | 3 +- components/zcash_address/src/kind/orchard.rs | 18 --- components/zcash_address/src/kind/unified.rs | 19 ++++ components/zcash_address/src/lib.rs | 12 +- 6 files changed, 72 insertions(+), 99 deletions(-) delete mode 100644 components/zcash_address/src/kind/orchard.rs create mode 100644 components/zcash_address/src/kind/unified.rs diff --git a/components/zcash_address/src/convert.rs b/components/zcash_address/src/convert.rs index 66e4664638..476140f6d6 100644 --- a/components/zcash_address/src/convert.rs +++ b/components/zcash_address/src/convert.rs @@ -59,9 +59,9 @@ pub trait FromAddress: Sized { Err(UnsupportedAddress("Sapling")) } - fn from_orchard(net: Network, data: orchard::Data) -> Result { + fn from_unified(net: Network, data: unified::Data) -> Result { let _ = (net, data); - Err(UnsupportedAddress("Orchard")) + Err(UnsupportedAddress("Unified")) } fn from_transparent_p2pkh(net: Network, data: p2pkh::Data) -> Result { @@ -111,7 +111,7 @@ pub trait ToAddress: private::Sealed { fn from_sapling(net: Network, data: sapling::Data) -> Self; - fn from_orchard(net: Network, data: orchard::Data) -> Self; + fn from_unified(net: Network, data: unified::Data) -> Self; fn from_transparent_p2pkh(net: Network, data: p2pkh::Data) -> Self; @@ -137,10 +137,10 @@ impl ToAddress for ZcashAddress { } } - fn from_orchard(net: Network, data: orchard::Data) -> Self { + fn from_unified(net: Network, data: unified::Data) -> Self { ZcashAddress { net, - kind: AddressKind::Orchard(data), + kind: AddressKind::Unified(data), } } diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index 2a898d335b..8195f8daaa 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -9,8 +9,6 @@ use crate::{kind::*, AddressKind, Network, ZcashAddress}; pub enum ParseError { /// The string is an invalid encoding. InvalidEncoding, - /// The string might be an unknown Zcash address from the future. - MaybeZcash, /// The string is not a Zcash address. NotZcash, } @@ -19,10 +17,6 @@ impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ParseError::InvalidEncoding => write!(f, "Invalid encoding"), - ParseError::MaybeZcash => write!( - f, - "This might be a Zcash address from the future that we don't know about" - ), ParseError::NotZcash => write!(f, "Not a Zcash address"), } } @@ -40,49 +34,47 @@ impl FromStr for ZcashAddress { // Most Zcash addresses use Bech32, so try that first. match bech32::decode(s) { - // Zcash addresses only use the original Bech32 variant, since the data - // corresponding to a particular HRP always has a fixed length. - Ok((_, _, Variant::Bech32m)) => return Err(ParseError::NotZcash), + Ok((hrp, data, Variant::Bech32m)) => { + // If we reached this point, the encoding is supposed to be valid Bech32m. + let data = + Vec::::from_base32(&data).map_err(|_| ParseError::InvalidEncoding)?; + + let net = match hrp.as_str() { + unified::MAINNET => Network::Main, + unified::TESTNET => Network::Test, + unified::REGTEST => Network::Regtest, + // We will not define new Bech32m address encodings. + _ => { + return Err(ParseError::NotZcash); + } + }; + + return data[..] + .try_into() + .map(AddressKind::Unified) + .map_err(|_| ParseError::InvalidEncoding) + .map(|kind| ZcashAddress { net, kind }); + } Ok((hrp, data, Variant::Bech32)) => { // If we reached this point, the encoding is supposed to be valid Bech32. let data = Vec::::from_base32(&data).map_err(|_| ParseError::InvalidEncoding)?; let net = match hrp.as_str() { - sapling::MAINNET | orchard::MAINNET => Network::Main, - sapling::TESTNET | orchard::TESTNET => Network::Test, - sapling::REGTEST | orchard::REGTEST => Network::Regtest, + sapling::MAINNET => Network::Main, + sapling::TESTNET => Network::Test, + sapling::REGTEST => Network::Regtest, + // We will not define new Bech32 address encodings. _ => { - // Use some heuristics to try and guess whether this might be a Zcash - // address from the future: - // - Zcash HRPs always start with a 'z'. - // - Zcash shielded addresses with diversification have data of - // length 43, but if we added the simple form of detection keys - // the data would have length 75. Alternatively if we switch from a - // 11-byte diversifier to two field elements, that would be 64 bytes. - return Err( - if hrp.starts_with('z') - && (data.len() == 43 || data.len() == 64 || data.len() == 75) - { - ParseError::MaybeZcash - } else { - ParseError::NotZcash - }, - ); + return Err(ParseError::NotZcash); } }; - return match hrp.as_str() { - sapling::MAINNET | sapling::TESTNET | sapling::REGTEST => { - data[..].try_into().map(AddressKind::Sapling) - } - orchard::MAINNET | orchard::TESTNET | orchard::REGTEST => { - data[..].try_into().map(AddressKind::Orchard) - } - _ => unreachable!(), - } - .map_err(|_| ParseError::InvalidEncoding) - .map(|kind| ZcashAddress { net, kind }); + return data[..] + .try_into() + .map(AddressKind::Sapling) + .map_err(|_| ParseError::InvalidEncoding) + .map(|kind| ZcashAddress { net, kind }); } Err(_) => (), } @@ -113,6 +105,10 @@ impl FromStr for ZcashAddress { } } +fn encode_bech32m(hrp: &str, data: &[u8]) -> String { + bech32::encode(hrp, data.to_base32(), Variant::Bech32m).expect("hrp is invalid") +} + fn encode_bech32(hrp: &str, data: &[u8]) -> String { bech32::encode(hrp, data.to_base32(), Variant::Bech32).expect("hrp is invalid") } @@ -143,11 +139,11 @@ impl fmt::Display for ZcashAddress { }, &data, ), - AddressKind::Orchard(data) => encode_bech32( + AddressKind::Unified(data) => encode_bech32m( match self.net { - Network::Main => orchard::MAINNET, - Network::Test => orchard::TESTNET, - Network::Regtest => orchard::REGTEST, + Network::Main => unified::MAINNET, + Network::Test => unified::TESTNET, + Network::Regtest => unified::REGTEST, }, &data, ), @@ -219,49 +215,30 @@ mod tests { } #[test] - fn orchard() { + fn unified() { encoding( "zo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq58lk79", ZcashAddress { net: Network::Main, - kind: AddressKind::Orchard([0; 43]), + kind: AddressKind::Unified([0; 43]), }, ); encoding( "ztestorchard1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqcrmt3p", ZcashAddress { net: Network::Test, - kind: AddressKind::Orchard([0; 43]), + kind: AddressKind::Unified([0; 43]), }, ); encoding( "zregtestorchard1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq88jxqx", ZcashAddress { net: Network::Regtest, - kind: AddressKind::Orchard([0; 43]), + kind: AddressKind::Unified([0; 43]), }, ); } - #[test] - fn maybe_zcash() { - assert_eq!( - "zmaybe1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqql7xs38" - .parse::(), - Err(ParseError::MaybeZcash), - ); - assert_eq!( - "zpossibly1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq559klt" - .parse::(), - Err(ParseError::MaybeZcash), - ); - assert_eq!( - "nope1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg8f5j9" - .parse::(), - Err(ParseError::NotZcash), - ); - } - #[test] fn transparent() { encoding( diff --git a/components/zcash_address/src/kind.rs b/components/zcash_address/src/kind.rs index 647ceec594..3999fad39a 100644 --- a/components/zcash_address/src/kind.rs +++ b/components/zcash_address/src/kind.rs @@ -1,4 +1,5 @@ -pub(crate) mod orchard; +pub(crate) mod unified; + pub(crate) mod sapling; pub(crate) mod sprout; diff --git a/components/zcash_address/src/kind/orchard.rs b/components/zcash_address/src/kind/orchard.rs deleted file mode 100644 index 3641fe17ae..0000000000 --- a/components/zcash_address/src/kind/orchard.rs +++ /dev/null @@ -1,18 +0,0 @@ -/// The HRP for a Bech32-encoded mainnet Orchard address. -/// -/// Defined in the [Zcash Protocol Specification section 5.6.4.1][orchardpaymentaddrencoding]. -/// -/// [orchardpaymentaddrencoding]: https://zips.z.cash/protocol/nu5.pdf#orchardpaymentaddrencoding -pub(crate) const MAINNET: &str = "zo"; - -/// The HRP for a Bech32-encoded testnet Orchard address. -/// -/// Defined in the [Zcash Protocol Specification section 5.6.4.1][orchardpaymentaddrencoding]. -/// -/// [orchardpaymentaddrencoding]: https://zips.z.cash/protocol/nu5.pdf#orchardpaymentaddrencoding -pub(crate) const TESTNET: &str = "ztestorchard"; - -/// The HRP for a Bech32-encoded regtest Orchard address. -pub(crate) const REGTEST: &str = "zregtestorchard"; - -pub(crate) type Data = [u8; 43]; diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs new file mode 100644 index 0000000000..35310099d3 --- /dev/null +++ b/components/zcash_address/src/kind/unified.rs @@ -0,0 +1,19 @@ +/// The HRP for a Bech32m-encoded mainnet Unified Address. +/// +/// Defined in [ZIP 316][zip-0316]. +/// +/// [zip-0316]: https://zips.z.cash/zip-0316 +pub(crate) const MAINNET: &str = "u"; + +/// The HRP for a Bech32m-encoded testnet Unified Address. +/// +/// Defined in [ZIP 316][zip-0316]. +/// +/// [zip-0316]: https://zips.z.cash/zip-0316 +pub(crate) const TESTNET: &str = "utest"; + +/// The HRP for a Bech32m-encoded regtest Unified Address. +pub(crate) const REGTEST: &str = "uregtest"; + +/// TODO +pub(crate) type Data = [u8; 43]; diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index cd239779de..8f8841aa1e 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -31,7 +31,7 @@ pub enum Network { enum AddressKind { Sprout(kind::sprout::Data), Sapling(kind::sapling::Data), - Orchard(kind::orchard::Data), + Unified(kind::unified::Data), P2pkh(kind::p2pkh::Data), P2sh(kind::p2sh::Data), } @@ -46,17 +46,11 @@ impl ZcashAddress { /// /// # Errors /// - /// In most cases, [`ParseError::NotZcash`] will be returned on failure. The two - /// exceptions are: - /// /// - If the parser can detect that the string _must_ contain an address encoding used /// by Zcash, [`ParseError::InvalidEncoding`] will be returned if any subsequent /// part of that encoding is invalid. /// - /// - [`ParseError::MaybeZcash`] will be returned if the string is Bech32-encoded data - /// that satisfies some heuristics for probable future Zcash address formats (such - /// as beginning with a `z`). This can either be treated as an indication that this - /// library dependency should be updated, or mapped to [`ParseError::NotZcash`]. + /// - In all other cases, [`ParseError::NotZcash`] will be returned on failure. /// /// # Examples /// @@ -87,7 +81,7 @@ impl ZcashAddress { match self.kind { AddressKind::Sprout(data) => T::from_sprout(self.net, data), AddressKind::Sapling(data) => T::from_sapling(self.net, data), - AddressKind::Orchard(data) => T::from_orchard(self.net, data), + AddressKind::Unified(data) => T::from_unified(self.net, data), AddressKind::P2pkh(data) => T::from_transparent_p2pkh(self.net, data), AddressKind::P2sh(data) => T::from_transparent_p2sh(self.net, data), } From 6717cd821c6e61ced2b02c18c115a29893cf45ac Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 20 May 2021 17:11:12 +0100 Subject: [PATCH 11/18] Move F4Jumble implementation into zcash_address --- components/zcash_address/Cargo.toml | 4 ++++ components/zcash_address/src/kind/unified.rs | 2 ++ .../zcash_address/src/kind/unified}/f4jumble.rs | 0 .../zcash_address/src/kind/unified}/f4jumble/test_vectors.rs | 0 zcash_primitives/src/address.rs | 2 -- zcash_primitives/src/lib.rs | 1 - 6 files changed, 6 insertions(+), 3 deletions(-) rename {zcash_primitives/src/address => components/zcash_address/src/kind/unified}/f4jumble.rs (100%) rename {zcash_primitives/src/address => components/zcash_address/src/kind/unified}/f4jumble/test_vectors.rs (100%) delete mode 100644 zcash_primitives/src/address.rs diff --git a/components/zcash_address/Cargo.toml b/components/zcash_address/Cargo.toml index bac4703acc..a02fc1feb6 100644 --- a/components/zcash_address/Cargo.toml +++ b/components/zcash_address/Cargo.toml @@ -13,4 +13,8 @@ edition = "2018" [dependencies] bech32 = "0.8" +blake2b_simd = "0.5" bs58 = { version = "0.4", features = ["check"] } + +[dev-dependencies] +proptest = "0.10.1" diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs index 35310099d3..bcc8668c3f 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -1,3 +1,5 @@ +mod f4jumble; + /// The HRP for a Bech32m-encoded mainnet Unified Address. /// /// Defined in [ZIP 316][zip-0316]. diff --git a/zcash_primitives/src/address/f4jumble.rs b/components/zcash_address/src/kind/unified/f4jumble.rs similarity index 100% rename from zcash_primitives/src/address/f4jumble.rs rename to components/zcash_address/src/kind/unified/f4jumble.rs diff --git a/zcash_primitives/src/address/f4jumble/test_vectors.rs b/components/zcash_address/src/kind/unified/f4jumble/test_vectors.rs similarity index 100% rename from zcash_primitives/src/address/f4jumble/test_vectors.rs rename to components/zcash_address/src/kind/unified/f4jumble/test_vectors.rs diff --git a/zcash_primitives/src/address.rs b/zcash_primitives/src/address.rs deleted file mode 100644 index 17613516d4..0000000000 --- a/zcash_primitives/src/address.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// Types and algorithms used in support of Zcash Unified Addresses -pub mod f4jumble; diff --git a/zcash_primitives/src/lib.rs b/zcash_primitives/src/lib.rs index 8bd5de7010..a2a113d404 100644 --- a/zcash_primitives/src/lib.rs +++ b/zcash_primitives/src/lib.rs @@ -9,7 +9,6 @@ // Temporary until we have addressed all Result cases. #![allow(clippy::result_unit_err)] -pub mod address; pub mod block; pub mod consensus; pub mod constants; From e982d7211f1c081ff65dd34ea6a87c7fc48f7f9b Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 20 May 2021 17:28:35 +0100 Subject: [PATCH 12/18] zcash_address: Implement Unified Address encoding / decoding --- components/zcash_address/src/convert.rs | 6 +- components/zcash_address/src/encoding.rs | 27 ++-- components/zcash_address/src/kind.rs | 2 +- components/zcash_address/src/kind/unified.rs | 124 ++++++++++++++++++- components/zcash_address/src/lib.rs | 7 +- 5 files changed, 145 insertions(+), 21 deletions(-) diff --git a/components/zcash_address/src/convert.rs b/components/zcash_address/src/convert.rs index 476140f6d6..ce29a0c3cc 100644 --- a/components/zcash_address/src/convert.rs +++ b/components/zcash_address/src/convert.rs @@ -59,7 +59,7 @@ pub trait FromAddress: Sized { Err(UnsupportedAddress("Sapling")) } - fn from_unified(net: Network, data: unified::Data) -> Result { + fn from_unified(net: Network, data: unified::Address) -> Result { let _ = (net, data); Err(UnsupportedAddress("Unified")) } @@ -111,7 +111,7 @@ pub trait ToAddress: private::Sealed { fn from_sapling(net: Network, data: sapling::Data) -> Self; - fn from_unified(net: Network, data: unified::Data) -> Self; + fn from_unified(net: Network, data: unified::Address) -> Self; fn from_transparent_p2pkh(net: Network, data: p2pkh::Data) -> Self; @@ -137,7 +137,7 @@ impl ToAddress for ZcashAddress { } } - fn from_unified(net: Network, data: unified::Data) -> Self { + fn from_unified(net: Network, data: unified::Address) -> Self { ZcashAddress { net, kind: AddressKind::Unified(data), diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index 8195f8daaa..56adbd0396 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -122,14 +122,14 @@ 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 { + let encoded = match &self.kind { AddressKind::Sprout(data) => encode_b58( if let Network::Main = self.net { sprout::MAINNET } else { sprout::TESTNET }, - &data, + data, ), AddressKind::Sapling(data) => encode_bech32( match self.net { @@ -137,7 +137,7 @@ impl fmt::Display for ZcashAddress { Network::Test => sapling::TESTNET, Network::Regtest => sapling::REGTEST, }, - &data, + data, ), AddressKind::Unified(data) => encode_bech32m( match self.net { @@ -145,7 +145,7 @@ impl fmt::Display for ZcashAddress { Network::Test => unified::TESTNET, Network::Regtest => unified::REGTEST, }, - &data, + &data.to_bytes(), ), AddressKind::P2pkh(data) => encode_b58( if let Network::Main = self.net { @@ -153,7 +153,7 @@ impl fmt::Display for ZcashAddress { } else { p2pkh::TESTNET }, - &data, + data, ), AddressKind::P2sh(data) => encode_b58( if let Network::Main = self.net { @@ -161,7 +161,7 @@ impl fmt::Display for ZcashAddress { } else { p2sh::TESTNET }, - &data, + data, ), }; write!(f, "{}", encoded) @@ -171,6 +171,7 @@ impl fmt::Display for ZcashAddress { #[cfg(test)] mod tests { use super::*; + use crate::kind::unified; fn encoding(encoded: &str, decoded: ZcashAddress) { assert_eq!(decoded.to_string(), encoded); @@ -217,24 +218,26 @@ mod tests { #[test] fn unified() { encoding( - "zo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq58lk79", + "u1cd8yzk5mdn4n9r8c24tp8j8e9ethw3rr7ker5zhew3kycyyxggdzfkcq5f9yf2jv8m5ar8krncsntlfpx3p4azvwrkp8z74t3vu4kqq2", ZcashAddress { net: Network::Main, - kind: AddressKind::Unified([0; 43]), + kind: AddressKind::Unified(unified::Address(vec![unified::Receiver::Sapling( + [0; 43], + )])), }, ); encoding( - "ztestorchard1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqcrmt3p", + "utest1cd8yzk5mdn4n9r8c24tp8j8e9ethw3rr7ker5zhew3kycyyxggdzfkcq5f9yf2jv8m5ar8krncsntlfpx3p4azvwrkp8z74t3vptphj8", ZcashAddress { net: Network::Test, - kind: AddressKind::Unified([0; 43]), + kind: AddressKind::Unified(unified::Address(vec![unified::Receiver::Sapling([0; 43])])), }, ); encoding( - "zregtestorchard1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq88jxqx", + "uregtest1cd8yzk5mdn4n9r8c24tp8j8e9ethw3rr7ker5zhew3kycyyxggdzfkcq5f9yf2jv8m5ar8krncsntlfpx3p4azvwrkp8z74t3vsnt5j0", ZcashAddress { net: Network::Regtest, - kind: AddressKind::Unified([0; 43]), + kind: AddressKind::Unified(unified::Address(vec![unified::Receiver::Sapling([0; 43])])), }, ); } diff --git a/components/zcash_address/src/kind.rs b/components/zcash_address/src/kind.rs index 3999fad39a..5397c027f8 100644 --- a/components/zcash_address/src/kind.rs +++ b/components/zcash_address/src/kind.rs @@ -1,4 +1,4 @@ -pub(crate) mod unified; +pub mod unified; pub(crate) mod sapling; pub(crate) mod sprout; diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs index bcc8668c3f..3e028b34f2 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -1,3 +1,8 @@ +use std::convert::{TryFrom, TryInto}; +use std::iter; + +use crate::{kind, ParseError}; + mod f4jumble; /// The HRP for a Bech32m-encoded mainnet Unified Address. @@ -17,5 +22,120 @@ pub(crate) const TESTNET: &str = "utest"; /// The HRP for a Bech32m-encoded regtest Unified Address. pub(crate) const REGTEST: &str = "uregtest"; -/// TODO -pub(crate) type Data = [u8; 43]; +const PADDING_LEN: usize = 16; + +/// The set of known Receivers for Unified Addresses. +/// +/// This enum is an internal-only type, and is maintained in preference order, so that the +/// derived [`PartialOrd`] will sort receivers correctly. From its documentation: +/// +/// > When derived on enums, variants are ordered by their top-to-bottom discriminant +/// > order. +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub(crate) enum Receiver { + Orchard([u8; 43]), + Sapling(kind::sapling::Data), + P2pkh(kind::p2pkh::Data), + P2sh(kind::p2sh::Data), + Unknown { typecode: u8, data: Vec }, +} + +impl TryFrom<(u8, &[u8])> for Receiver { + type Error = ParseError; + + fn try_from((typecode, addr): (u8, &[u8])) -> Result { + match typecode { + 0x00 => addr.try_into().map(Receiver::P2pkh), + 0x01 => addr.try_into().map(Receiver::P2sh), + 0x02 => addr.try_into().map(Receiver::Sapling), + 0x03 => addr.try_into().map(Receiver::Orchard), + _ => Ok(Receiver::Unknown { + typecode, + data: addr.to_vec(), + }), + } + .map_err(|_| ParseError::InvalidEncoding) + } +} + +impl Receiver { + fn typecode(&self) -> u8 { + match self { + Receiver::P2pkh(_) => 0x00, + Receiver::P2sh(_) => 0x01, + Receiver::Sapling(_) => 0x02, + Receiver::Orchard(_) => 0x03, + Receiver::Unknown { typecode, .. } => *typecode, + } + } + + fn addr(&self) -> &[u8] { + match self { + Receiver::P2pkh(data) => data, + Receiver::P2sh(data) => data, + Receiver::Sapling(data) => data, + Receiver::Orchard(data) => data, + Receiver::Unknown { data, .. } => data, + } + } +} + +/// A Unified Address. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Address(pub(crate) Vec); + +impl TryFrom<&[u8]> for Address { + type Error = ParseError; + + fn try_from(buf: &[u8]) -> Result { + let encoded = f4jumble::f4jumble_inv(buf).ok_or(ParseError::InvalidEncoding)?; + + // Validate and strip trailing zero bytes. + let encoded = match encoded.split_at(encoded.len() - PADDING_LEN) { + (encoded, tail) if tail == &[0; PADDING_LEN][..] => Ok(encoded), + _ => Err(ParseError::InvalidEncoding), + }?; + + iter::repeat(()) + .scan(encoded, |encoded, _| match encoded { + // Base case: we've parsed the full encoding. + [] => None, + // The raw encoding of a Unified Address is a concatenation of: + // - typecode: byte + // - length: byte + // - addr: byte[length] + [typecode, length, data @ ..] if data.len() >= *length as usize => { + let (addr, rest) = data.split_at(*length as usize); + *encoded = rest; + Some((*typecode, addr).try_into()) + } + // The encoding is truncated. + _ => Some(Err(ParseError::InvalidEncoding)), + }) + .collect::>() + .map(Address) + } +} + +impl Address { + /// Returns the raw encoding of this Unified Address. + pub(crate) fn to_bytes(&self) -> Vec { + self.0 + .iter() + .flat_map(|receiver| { + let addr = receiver.addr(); + // Holds by construction. + assert!(addr.len() < 256); + + let encoded: Vec<_> = iter::empty() + .chain(Some(receiver.typecode())) + .chain(Some(addr.len() as u8)) + .chain(addr.into_iter().cloned()) + .chain(iter::repeat(0).take(PADDING_LEN)) + .collect(); + + f4jumble::f4jumble(&encoded).unwrap() + }) + .collect() + } +} diff --git a/components/zcash_address/src/lib.rs b/components/zcash_address/src/lib.rs index 8f8841aa1e..f461d2e856 100644 --- a/components/zcash_address/src/lib.rs +++ b/components/zcash_address/src/lib.rs @@ -4,9 +4,10 @@ mod kind; pub use convert::{FromAddress, ToAddress, UnsupportedAddress}; pub use encoding::ParseError; +pub use kind::unified; /// A Zcash address. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct ZcashAddress { net: Network, kind: AddressKind, @@ -27,11 +28,11 @@ pub enum Network { } /// Known kinds of Zcash addresses. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] enum AddressKind { Sprout(kind::sprout::Data), Sapling(kind::sapling::Data), - Unified(kind::unified::Data), + Unified(unified::Address), P2pkh(kind::p2pkh::Data), P2sh(kind::p2sh::Data), } From c2b57048bf3f0bdf889e5a52084bfb75c9bce942 Mon Sep 17 00:00:00 2001 From: str4d Date: Tue, 25 May 2021 16:37:25 +0100 Subject: [PATCH 13/18] Update components/zcash_address/README.md Co-authored-by: Daira Hopwood --- components/zcash_address/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/zcash_address/README.md b/components/zcash_address/README.md index 46867e3e91..ead489de4d 100644 --- a/components/zcash_address/README.md +++ b/components/zcash_address/README.md @@ -1,6 +1,7 @@ # zcash_address -TBD +Zcash address parsing and serialization. This library allows its users to easily +recognize and give good error messages for new Zcash address types. ## License From ff8695de03f03ec7aed24fdb21bb78200f0a326b Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 25 May 2021 20:03:25 +0100 Subject: [PATCH 14/18] zcash_address: Add failing test showing unified::Address encoding bug --- .../proptest-regressions/kind/unified.txt | 7 +++ components/zcash_address/src/kind/unified.rs | 55 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 components/zcash_address/proptest-regressions/kind/unified.txt diff --git a/components/zcash_address/proptest-regressions/kind/unified.txt b/components/zcash_address/proptest-regressions/kind/unified.txt new file mode 100644 index 0000000000..f70dff62c0 --- /dev/null +++ b/components/zcash_address/proptest-regressions/kind/unified.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc e08469bc301313ef868b97a5c37d9a9746d9720c915a9127c89db25c3be778fd # shrinks to ua = Address([Sapling([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), P2pkh([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])]) diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs index 3e028b34f2..560208802d 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -139,3 +139,58 @@ impl Address { .collect() } } + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use proptest::{ + array::{uniform11, uniform20, uniform32}, + prelude::*, + }; + + use super::{Address, Receiver}; + + prop_compose! { + fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] { + let mut c = [0; 43]; + c[..11].copy_from_slice(&a); + c[11..].copy_from_slice(&b); + c + } + } + + fn arb_shielded_receiver() -> BoxedStrategy { + prop_oneof![ + uniform43().prop_map(Receiver::Sapling), + uniform43().prop_map(Receiver::Orchard), + ] + .boxed() + } + + fn arb_transparent_receiver() -> BoxedStrategy { + prop_oneof![ + uniform20(0u8..).prop_map(Receiver::P2pkh), + uniform20(0u8..).prop_map(Receiver::P2sh), + ] + .boxed() + } + + prop_compose! { + fn arb_unified_address()( + shielded in prop::collection::hash_set(arb_shielded_receiver(), 1..2), + transparent in prop::option::of(arb_transparent_receiver()), + ) -> Address { + Address(shielded.into_iter().chain(transparent).collect()) + } + } + + proptest! { + #[test] + fn ua_roundtrip(ua in arb_unified_address()) { + let bytes = ua.to_bytes(); + let decoded = Address::try_from(&bytes[..]); + prop_assert_eq!(decoded, Ok(ua)); + } + } +} From ff94f66d8e2b6e6efd89bfd2388d5a2c4c5887f2 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 25 May 2021 21:23:15 +0100 Subject: [PATCH 15/18] zcash_address: Fix padding and F4Jumble positions in Address::to_bytes These need to be applied to the entire UA encoding, not to the encoding of each individual receiver. --- components/zcash_address/src/kind/unified.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs index 560208802d..5c7b5e6a89 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -120,23 +120,23 @@ impl TryFrom<&[u8]> for Address { impl Address { /// Returns the raw encoding of this Unified Address. pub(crate) fn to_bytes(&self) -> Vec { - self.0 + let encoded: Vec<_> = self + .0 .iter() .flat_map(|receiver| { let addr = receiver.addr(); // Holds by construction. assert!(addr.len() < 256); - let encoded: Vec<_> = iter::empty() + iter::empty() .chain(Some(receiver.typecode())) .chain(Some(addr.len() as u8)) .chain(addr.into_iter().cloned()) - .chain(iter::repeat(0).take(PADDING_LEN)) - .collect(); - - f4jumble::f4jumble(&encoded).unwrap() }) - .collect() + .chain(iter::repeat(0).take(PADDING_LEN)) + .collect(); + + f4jumble::f4jumble(&encoded).unwrap() } } From f36034cfac48477926a3b9032e4370a484bd9f4a Mon Sep 17 00:00:00 2001 From: str4d Date: Sun, 6 Jun 2021 23:54:11 +0100 Subject: [PATCH 16/18] zcash_address: Use exhaustive matching in Display impl Co-authored-by: Daira Hopwood --- components/zcash_address/src/encoding.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index 56adbd0396..a7c7722149 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -124,10 +124,9 @@ impl fmt::Display for ZcashAddress { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let encoded = match &self.kind { AddressKind::Sprout(data) => encode_b58( - if let Network::Main = self.net { - sprout::MAINNET - } else { - sprout::TESTNET + match self.net { + Network::Main => sprout::MAINNET, + Network::Test | Network::Regtest => sprout::TESTNET, }, data, ), @@ -148,18 +147,16 @@ impl fmt::Display for ZcashAddress { &data.to_bytes(), ), AddressKind::P2pkh(data) => encode_b58( - if let Network::Main = self.net { - p2pkh::MAINNET - } else { - p2pkh::TESTNET + match self.net { + Network::Main => p2pkh::MAINNET, + Network::Test | Network::Regtest => p2pkh::TESTNET, }, data, ), AddressKind::P2sh(data) => encode_b58( - if let Network::Main = self.net { - p2sh::MAINNET - } else { - p2sh::TESTNET + match self.net { + Network::Main => p2sh::MAINNET, + Network::Test | Network::Regtest => p2sh::TESTNET, }, data, ), From 1590565f048be15f6bd5fda97a8063a76df1c38d Mon Sep 17 00:00:00 2001 From: str4d Date: Sun, 6 Jun 2021 23:58:52 +0100 Subject: [PATCH 17/18] zcash_address: Minor cleanups Co-authored-by: Daira Hopwood --- components/zcash_address/src/encoding.rs | 14 ++++++-------- components/zcash_address/src/kind/unified.rs | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/components/zcash_address/src/encoding.rs b/components/zcash_address/src/encoding.rs index a7c7722149..ab7d7768ba 100644 --- a/components/zcash_address/src/encoding.rs +++ b/components/zcash_address/src/encoding.rs @@ -32,7 +32,7 @@ impl FromStr for ZcashAddress { // Remove leading and trailing whitespace, to handle copy-paste errors. let s = s.trim(); - // Most Zcash addresses use Bech32, so try that first. + // Most Zcash addresses use Bech32 or Bech32m, so try those first. match bech32::decode(s) { Ok((hrp, data, Variant::Bech32m)) => { // If we reached this point, the encoding is supposed to be valid Bech32m. @@ -114,10 +114,10 @@ fn encode_bech32(hrp: &str, data: &[u8]) -> String { } fn encode_b58(prefix: [u8; 2], data: &[u8]) -> String { - let mut decoded = Vec::with_capacity(2 + data.len()); - decoded.extend_from_slice(&prefix); - decoded.extend_from_slice(data); - bs58::encode(decoded).with_check().into_string() + let mut bytes = Vec::with_capacity(2 + data.len()); + bytes.extend_from_slice(&prefix); + bytes.extend_from_slice(data); + bs58::encode(bytes).with_check().into_string() } impl fmt::Display for ZcashAddress { @@ -218,9 +218,7 @@ mod tests { "u1cd8yzk5mdn4n9r8c24tp8j8e9ethw3rr7ker5zhew3kycyyxggdzfkcq5f9yf2jv8m5ar8krncsntlfpx3p4azvwrkp8z74t3vu4kqq2", ZcashAddress { net: Network::Main, - kind: AddressKind::Unified(unified::Address(vec![unified::Receiver::Sapling( - [0; 43], - )])), + kind: AddressKind::Unified(unified::Address(vec![unified::Receiver::Sapling([0; 43])])), }, ); encoding( diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs index 5c7b5e6a89..8569f2071d 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -107,7 +107,7 @@ impl TryFrom<&[u8]> for Address { [typecode, length, data @ ..] if data.len() >= *length as usize => { let (addr, rest) = data.split_at(*length as usize); *encoded = rest; - Some((*typecode, addr).try_into()) + Some(Receiver::try_from((*typecode, addr))) } // The encoding is truncated. _ => Some(Err(ParseError::InvalidEncoding)), From af02e1142bfe736079bf060a063d15d43a403271 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 7 Jun 2021 00:13:21 +0100 Subject: [PATCH 18/18] zcash_address: Fix clippy lint --- components/zcash_address/src/kind/unified.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/zcash_address/src/kind/unified.rs b/components/zcash_address/src/kind/unified.rs index 8569f2071d..8a50212e4e 100644 --- a/components/zcash_address/src/kind/unified.rs +++ b/components/zcash_address/src/kind/unified.rs @@ -131,7 +131,7 @@ impl Address { iter::empty() .chain(Some(receiver.typecode())) .chain(Some(addr.len() as u8)) - .chain(addr.into_iter().cloned()) + .chain(addr.iter().cloned()) }) .chain(iter::repeat(0).take(PADDING_LEN)) .collect();