From f94808cb53aedd349c7d2190ed0fe67e24031b02 Mon Sep 17 00:00:00 2001 From: Joy Wang <108701016+joyqvq@users.noreply.github.com> Date: Thu, 14 Dec 2023 16:11:39 -0500 Subject: [PATCH 1/2] fastcrypto: impl bech32 encoding --- Cargo.lock | 7 ++++ fastcrypto/Cargo.toml | 1 + fastcrypto/src/encoding.rs | 20 ++++++++++ fastcrypto/src/tests/encoding_tests.rs | 55 +++++++++++++++++++++++++- 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 8cad9e7e2b..ed028e4cde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -423,6 +423,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bellpepper" version = "0.4.0" @@ -1389,6 +1395,7 @@ dependencies = [ "base64 0.21.0", "base64ct", "bcs", + "bech32", "bincode", "blake2", "blake3", diff --git a/fastcrypto/Cargo.toml b/fastcrypto/Cargo.toml index 9db6cfbd13..902fb7ccdc 100644 --- a/fastcrypto/Cargo.toml +++ b/fastcrypto/Cargo.toml @@ -62,6 +62,7 @@ lazy_static = "1.4.0" fastcrypto-derive = { path = "../fastcrypto-derive", version = "0.1.3" } serde_json = "1.0.93" num-bigint = "0.4.4" +bech32 = "0.9.1" [[bench]] name = "crypto" diff --git a/fastcrypto/src/encoding.rs b/fastcrypto/src/encoding.rs index e12ad8c588..1f7d35da59 100644 --- a/fastcrypto/src/encoding.rs +++ b/fastcrypto/src/encoding.rs @@ -13,6 +13,7 @@ //! ``` use base64ct::Encoding as _; +use bech32::{FromBase32, Variant}; use eyre::{eyre, Result}; use schemars::JsonSchema; use serde; @@ -266,3 +267,22 @@ impl<'de, const N: usize> DeserializeAs<'de, [u8; N]> for Base58 { Ok(array) } } + +/// Bech32 encoding +pub struct Bech32(String); + +impl Bech32 { + pub fn decode(s: &str, hrp: &str) -> Result, eyre::Report> { + let (parsed, data, variant) = bech32::decode(s).map_err(|e| eyre::eyre!(e))?; + if parsed != hrp || variant != Variant::Bech32 { + Err(eyre!("invalid hrp or variant")) + } else { + Vec::::from_base32(&data).map_err(|e| eyre::eyre!(e)) + } + } + + pub fn encode>(data: T, hrp: &str) -> Result { + use bech32::ToBase32; + bech32::encode(hrp, data.to_base32(), Variant::Bech32).map_err(|e| eyre::eyre!(e)) + } +} diff --git a/fastcrypto/src/tests/encoding_tests.rs b/fastcrypto/src/tests/encoding_tests.rs index 59449b02a6..83d63d1c0e 100644 --- a/fastcrypto/src/tests/encoding_tests.rs +++ b/fastcrypto/src/tests/encoding_tests.rs @@ -1,11 +1,25 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use crate::encoding::{encode_with_format, Base58, Base64, Encoding, Hex}; +use crate::encoding::{encode_with_format, Base58, Base64, Bech32, Encoding, Hex}; use proptest::{arbitrary::Arbitrary, prop_assert_eq}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +macro_rules! check_valid_address_roundtrip { + ($($test_name:ident, $addr:literal);* $(;)?) => { + $( + #[test] + #[cfg(feature = "alloc")] + fn $test_name() { + let decoded = Bech32::decode($addr, "bc").unwrap(); + let encoded = Bech32::encode(decoded, "bc").unwrap(); + assert_eq!(encoded, $addr); + } + )* + } +} + #[test] fn test_hex_roundtrip() { let bytes = &[1, 10, 100]; @@ -143,6 +157,38 @@ fn test_base58_err() { assert!(Base58::try_from("invalid\0".to_string()).is_err()); } +#[test] +fn test_bech32() { + // Test vectors from https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#test-vectors + let bytes = [0; 32]; + let encoded = Bech32::encode(bytes, "suiprivkey").unwrap(); + let decoded = Bech32::decode(&encoded, "suiprivkey").unwrap(); + assert_eq!(bytes, decoded.as_slice()); + + assert!(Bech32::decode("A12UEL5L", "a").is_ok()); + assert!(Bech32::decode("a12uel5l", "a").is_ok()); + assert!(Bech32::decode("an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio").is_ok()); + assert!(Bech32::decode("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", "abcdef").is_ok()); + assert!(Bech32::decode("11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", "1").is_ok()); + assert!(Bech32::decode( + "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", + "split" + ) + .is_ok()); + assert!(Bech32::decode("?1ezyfcl", "?").is_ok()); + assert!(Bech32::decode("an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", "an84characterslonghumanreadablepartthatcontainsthenumber").is_err()); + assert!(Bech32::decode("pzry9x0s0muk", "").is_err()); + + check_valid_address_roundtrip! { + bip_173_valid_address_roundtrip_0, "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"; + bip_173_valid_address_roundtrip_1, "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"; + bip_173_valid_address_roundtrip_2, "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx"; + bip_173_valid_address_roundtrip_3, "BC1SW50QA3JX3S"; + bip_173_valid_address_roundtrip_4, "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj"; + bip_173_valid_address_roundtrip_5, "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"; + } +} + proptest::proptest! { #[test] fn roundtrip_hex(bytes in <[u8; 20]>::arbitrary()) { @@ -164,4 +210,11 @@ proptest::proptest! { let decoded = Base58::decode(&encoded).unwrap(); prop_assert_eq!(bytes, decoded.as_slice()); } + + #[test] + fn roundtrip_bech32(bytes in <[u8; 20]>::arbitrary()) { + let encoded = Bech32::encode(bytes, "suiprivkey").unwrap(); + let decoded = Bech32::decode(&encoded, "suiprivkey").unwrap(); + prop_assert_eq!(bytes, decoded.as_slice()); + } } From 42de4b56b3752d16e8aa97d48b29f92b2d00ec60 Mon Sep 17 00:00:00 2001 From: Joy Wang <108701016+joyqvq@users.noreply.github.com> Date: Tue, 19 Dec 2023 14:36:29 -0500 Subject: [PATCH 2/2] add examples --- fastcrypto/src/encoding.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/fastcrypto/src/encoding.rs b/fastcrypto/src/encoding.rs index 1f7d35da59..f8c355f36b 100644 --- a/fastcrypto/src/encoding.rs +++ b/fastcrypto/src/encoding.rs @@ -272,6 +272,13 @@ impl<'de, const N: usize> DeserializeAs<'de, [u8; N]> for Base58 { pub struct Bech32(String); impl Bech32 { + /// Decodes the Bech32 string to bytes, validating the given human readable part (hrp). See spec: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki + /// # Example: + /// ``` + /// use fastcrypto::encoding::Bech32; + /// let bytes = Bech32::decode("split1qqqqsk5gh5","split").unwrap(); + /// assert_eq!(bytes, vec![0, 0]); + /// ``` pub fn decode(s: &str, hrp: &str) -> Result, eyre::Report> { let (parsed, data, variant) = bech32::decode(s).map_err(|e| eyre::eyre!(e))?; if parsed != hrp || variant != Variant::Bech32 { @@ -281,6 +288,13 @@ impl Bech32 { } } + /// Encodes bytes into a Bech32 encoded string, with the given human readable part (hrp). See spec: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki + /// # Example: + /// ``` + /// use fastcrypto::encoding::Bech32; + /// let str = Bech32::encode(vec![0, 0],"split").unwrap(); + /// assert_eq!(str, "split1qqqqsk5gh5".to_string()); + /// ``` pub fn encode>(data: T, hrp: &str) -> Result { use bech32::ToBase32; bech32::encode(hrp, data.to_base32(), Variant::Bech32).map_err(|e| eyre::eyre!(e))