From 3e7dcf6a9b84ac90424a95117b5e59e882ddad3b Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Fri, 20 Sep 2024 20:38:53 +0200 Subject: [PATCH] feat(models): add support for base64-encoded `StdAddr` --- Cargo.toml | 9 +- benches/mine.rs | 4 +- fuzz/Cargo.toml | 7 + fuzz/fuzz_targets/base64_addr.rs | 14 ++ src/dict/aug.rs | 4 +- src/dict/typed.rs | 4 +- src/error.rs | 3 + src/lib.rs | 36 ++-- src/models/message/address.rs | 276 +++++++++++++++++++++++++++++++ src/util.rs | 36 ++++ 10 files changed, 367 insertions(+), 26 deletions(-) create mode 100644 fuzz/fuzz_targets/base64_addr.rs diff --git a/Cargo.toml b/Cargo.toml index 242736b1..686f488d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,14 +37,14 @@ harness = false members = ["proc"] [dependencies] -ahash = "0.8" +ahash = "0.8.11" anyhow = { version = "1.0", optional = true } base64 = { version = "0.22", optional = true } bitflags = "2.3" blake3 = { version = "1.5", optional = true } bytes = { version = "1.4", optional = true } -crc32c = "0.6" -ed25519-dalek = { version = "2.0", optional = true } +crc32c = "0.6.8" +ed25519-dalek = { version = "2.1.1", optional = true } everscale-crypto = { version = "0.2", features = ["tl-proto"], optional = true } hex = "0.4" num-bigint = { version = "0.4", optional = true } @@ -112,3 +112,6 @@ opt-level = 1 [package.metadata.docs.rs] features = ["base64", "serde", "models", "sync", "stats", "abi"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] } diff --git a/benches/mine.rs b/benches/mine.rs index 6af40722..0ce27453 100644 --- a/benches/mine.rs +++ b/benches/mine.rs @@ -189,7 +189,9 @@ fn mine_address(c: &mut Criterion) { group.bench_with_input(BenchmarkId::from_parameter(nonce), &nonce, move |b, _| { b.iter(|| { thread_local! { - static CODE_CELL: std::cell::UnsafeCell> = std::cell::UnsafeCell::new(None); + static CODE_CELL: std::cell::UnsafeCell> = const { + std::cell::UnsafeCell::new(None) + }; } CODE_CELL.with(|code_cell| { diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 7b2cd142..7c551449 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -13,11 +13,18 @@ libfuzzer-sys = "0.4" [dependencies.everscale-types] path = ".." +features = ["base64"] # Prevent this from interfering with workspaces [workspace] members = ["."] +[[bin]] +name = "base64_addr" +path = "fuzz_targets/base64_addr.rs" +test = false +doc = false + [[bin]] name = "boc_decode" path = "fuzz_targets/boc_decode.rs" diff --git a/fuzz/fuzz_targets/base64_addr.rs b/fuzz/fuzz_targets/base64_addr.rs new file mode 100644 index 00000000..e90f8483 --- /dev/null +++ b/fuzz/fuzz_targets/base64_addr.rs @@ -0,0 +1,14 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +use everscale_types::models::{StdAddr, StdAddrFormat}; +use libfuzzer_sys::Corpus; + +fuzz_target!(|data: &[u8]| -> Corpus { + if let Ok(s) = std::str::from_utf8(data) { + if StdAddr::from_str_ext(s, StdAddrFormat::any()).is_ok() { + return Corpus::Keep; + } + } + Corpus::Reject +}); diff --git a/src/dict/aug.rs b/src/dict/aug.rs index 06385ab5..793bcb85 100644 --- a/src/dict/aug.rs +++ b/src/dict/aug.rs @@ -592,7 +592,7 @@ where /// /// [`values`]: Dict::values /// [`raw_values`]: Dict::raw_values - pub fn iter<'a>(&'a self) -> AugIter<'_, K, A, V> + pub fn iter<'a>(&'a self) -> AugIter<'a, K, A, V> where V: Load<'a>, { @@ -1017,7 +1017,7 @@ mod tests { (4258889371, SomeValue(4956495), 3256452222), ], ] { - let result = AugDict::::try_from_sorted_slice(&entries).unwrap(); + let result = AugDict::::try_from_sorted_slice(entries).unwrap(); let mut dict = AugDict::::new(); for (k, a, v) in entries { diff --git a/src/dict/typed.rs b/src/dict/typed.rs index f5b8c781..f3c9add6 100644 --- a/src/dict/typed.rs +++ b/src/dict/typed.rs @@ -461,7 +461,7 @@ where /// /// [`values`]: Dict::values /// [`raw_values`]: Dict::raw_values - pub fn iter<'a>(&'a self) -> Iter<'_, K, V> + pub fn iter<'a>(&'a self) -> Iter<'a, K, V> where V: Load<'a>, { @@ -478,7 +478,7 @@ where /// /// In the current implementation, iterating over dictionary builds a key /// for each element. - pub fn iter_union<'a>(&'a self, other: &'a Self) -> UnionIter<'_, K, V> + pub fn iter_union<'a>(&'a self, other: &'a Self) -> UnionIter<'a, K, V> where V: Load<'a>, { diff --git a/src/error.rs b/src/error.rs index 3eeb13e8..a1d98492 100644 --- a/src/error.rs +++ b/src/error.rs @@ -85,6 +85,9 @@ pub enum ParseAddrError { /// Too many address parts. #[error("unexpected address part")] UnexpectedPart, + /// Unexpected or invalid address format. + #[error("invalid address format")] + BadFormat, } /// Error type for block id parsing related errors. diff --git a/src/lib.rs b/src/lib.rs index 2c20b95c..a22d1eae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,18 +13,18 @@ //! ## `Cell` vs `CellSlice` vs `CellBuilder` //! //! - [`Cell`] is an immutable tree and provides only basic methods for accessing -//! nodes and some meta info. +//! nodes and some meta info. //! //! - [`CellSlice`] is a read-only view for a part of some cell. It can only -//! be obtained from an existing cell. A cell contains **up to 1023 bits** and -//! **up to 4 references**. Minimal data unit is bit, so a cell slice is similar -//! to a couple of ranges (bit range and refs range). +//! be obtained from an existing cell. A cell contains **up to 1023 bits** and +//! **up to 4 references**. Minimal data unit is bit, so a cell slice is similar +//! to a couple of ranges (bit range and refs range). //! //! - [`CellBuilder`] is used to create a new cell. It is used as an append-only -//! data structure and is the only way to create a new cell with the provided data. -//! Cell creation depends on a context (e.g. message creation in a wallet or a -//! TVM execution with gas tracking), so [`CellBuilder::build_ext`] accepts -//! a [`CellContext`] parameter which can be used to track and modify cells creation. +//! data structure and is the only way to create a new cell with the provided data. +//! Cell creation depends on a context (e.g. message creation in a wallet or a +//! TVM execution with gas tracking), so [`CellBuilder::build_ext`] accepts +//! a [`CellContext`] parameter which can be used to track and modify cells creation. //! //! ## BOC //! @@ -36,15 +36,15 @@ //! ### Merkle stuff //! //! - Pruned branch is a "building block" of merkle structures. A single pruned branch -//! cell replaces a whole subtree and contains just the hash of its root cell hash. +//! cell replaces a whole subtree and contains just the hash of its root cell hash. //! //! - [`MerkleProof`] contains a subset of original tree of cells. In most cases -//! it is created from [`UsageTree`] of some visited cells. Merkle proof is used -//! to proof that something was presented in the origin tree and provide some additional -//! context. +//! it is created from [`UsageTree`] of some visited cells. Merkle proof is used +//! to proof that something was presented in the origin tree and provide some additional +//! context. //! //! - [`MerkleUpdate`] describes a difference between two trees of cells. It can be -//! applied to old cell to create a new cell. +//! applied to old cell to create a new cell. //! //! ### Numeric stuff //! @@ -69,15 +69,15 @@ //! All models implement [`Load`] and [`Store`] traits for conversion from/to cells. //! //! - [`RawDict`] constrains only key size in bits. It is useful when a dictionary -//! can contain multiple types of values. +//! can contain multiple types of values. //! //! - [`Dict`] is a strongly typed version of definition and is a preferable way -//! of working with this data structure. Key type must implement [`DictKey`] trait, -//! which is implemented for numbers and addresses. +//! of working with this data structure. Key type must implement [`DictKey`] trait, +//! which is implemented for numbers and addresses. //! //! - [`AugDict`] adds additional values for all nodes. You can use it to quickly -//! access a subtotal of values for each subtree. -//! NOTE: this type is partially implemented due to its complexity. +//! access a subtotal of values for each subtree. +//! NOTE: this type is partially implemented due to its complexity. //! //! ## Supported Rust Versions //! diff --git a/src/models/message/address.rs b/src/models/message/address.rs index 39fbcfe5..eb7654b9 100644 --- a/src/models/message/address.rs +++ b/src/models/message/address.rs @@ -238,6 +238,51 @@ impl StdAddr { } } + /// Parses a base64-encoded address. + #[cfg(feature = "base64")] + pub fn from_str_ext( + s: &str, + format: StdAddrFormat, + ) -> Result<(Self, Base64StdAddrFlags), ParseAddrError> { + use base64::prelude::{Engine as _, BASE64_STANDARD, BASE64_URL_SAFE}; + + match s.len() { + 0 => Err(ParseAddrError::Empty), + 66..=69 if format.allow_raw => Self::from_str(s).map(|addr| (addr, Default::default())), + 48 if format.allow_base64 || format.allow_base64_url => { + let mut buffer = [0u8; 36]; + + let base64_url = s.contains(['_', '-']); + + let Ok(36) = if base64_url { + BASE64_URL_SAFE + } else { + BASE64_STANDARD + } + .decode_slice(s, &mut buffer) else { + return Err(ParseAddrError::BadFormat); + }; + + let crc = crc_16(&buffer[..34]); + if buffer[34] as u16 != (crc >> 8) || buffer[35] as u16 != (crc & 0xff) { + return Err(ParseAddrError::BadFormat); + } + + let addr = StdAddr::new( + buffer[1] as i8, + HashBytes(buffer[2..34].try_into().unwrap()), + ); + let flags = Base64StdAddrFlags { + testnet: buffer[0] & 0x80 != 0, + base64_url, + bounceable: buffer[0] & 0x40 == 0, + }; + Ok((addr, flags)) + } + _ => Err(ParseAddrError::BadFormat), + } + } + /// Returns `true` if this address is for a masterchain block. /// /// See [`ShardIdent::MASTERCHAIN`] @@ -259,6 +304,32 @@ impl StdAddr { pub const fn prefix(&self) -> u64 { u64::from_be_bytes(*self.address.first_chunk()) } + + /// Returns a pretty-printer for base64-encoded address. + #[cfg(feature = "base64")] + pub const fn display_base64(&self, bounceable: bool) -> DisplayBase64StdAddr<'_> { + DisplayBase64StdAddr { + addr: self, + flags: Base64StdAddrFlags { + testnet: false, + base64_url: false, + bounceable, + }, + } + } + + /// Returns a pretty-printer for URL-safe base64-encoded address. + #[cfg(feature = "base64")] + pub const fn display_base64_url(&self, bounceable: bool) -> DisplayBase64StdAddr<'_> { + DisplayBase64StdAddr { + addr: self, + flags: Base64StdAddrFlags { + testnet: false, + base64_url: true, + bounceable, + }, + } + } } impl Addr for StdAddr { @@ -464,6 +535,132 @@ impl<'de> serde::Deserialize<'de> for StdAddr { } } +/// A helper struct to work with base64-encoded addresses. +#[cfg(feature = "base64")] +pub struct StdAddrBase64Repr; + +#[cfg(all(feature = "base64", feature = "serde"))] +impl StdAddrBase64Repr { + /// Serializes address into a base64-encoded string. + pub fn serialize(addr: &StdAddr, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(&DisplayBase64StdAddr { + addr, + flags: Base64StdAddrFlags { + testnet: false, + base64_url: URL_SAFE, + bounceable: false, + }, + }) + } + + /// Deserializes address as a base64-encoded string. + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{Error, Visitor}; + + struct StdAddrBase64Visitor; + + impl<'de> Visitor<'de> for StdAddrBase64Visitor { + type Value = StdAddr; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("a standard address") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + StdAddr::from_str_ext(v, StdAddrFormat::any()) + .map(|(addr, _)| addr) + .map_err(E::custom) + } + } + + deserializer.deserialize_str(StdAddrBase64Visitor) + } +} + +/// Parsing options for [`StdAddr::from_str_ext`] +#[cfg(feature = "base64")] +#[derive(Debug, Clone, Copy)] +pub struct StdAddrFormat { + /// Allow raw address (0:000...000). + pub allow_raw: bool, + /// Allow base64-encoded address. + pub allow_base64: bool, + /// Allow URL-safe base64 encoding. + pub allow_base64_url: bool, +} + +#[cfg(feature = "base64")] +impl StdAddrFormat { + /// Allows any address format. + pub const fn any() -> Self { + StdAddrFormat { + allow_raw: true, + allow_base64: true, + allow_base64_url: true, + } + } +} + +/// Base64-encoded address flags. +#[cfg(feature = "base64")] +#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] +pub struct Base64StdAddrFlags { + /// Address belongs to testnet. + pub testnet: bool, + /// Use URL-safe base64 encoding. + pub base64_url: bool, + /// Whether to set `bounce` flag during transfer. + pub bounceable: bool, +} + +/// Pretty-printer for [`StdAddr`] in base64 format. +#[cfg(feature = "base64")] +pub struct DisplayBase64StdAddr<'a> { + /// Address to display. + pub addr: &'a StdAddr, + /// Encoding flags. + pub flags: Base64StdAddrFlags, +} + +#[cfg(feature = "base64")] +impl std::fmt::Display for DisplayBase64StdAddr<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use base64::prelude::{Engine as _, BASE64_STANDARD, BASE64_URL_SAFE}; + + let mut buffer = [0u8; 36]; + buffer[0] = (0x51 - (self.flags.bounceable as i32) * 0x40 + + (self.flags.testnet as i32) * 0x80) as u8; + buffer[1] = self.addr.workchain as u8; + buffer[2..34].copy_from_slice(self.addr.address.as_array()); + + let crc = crc_16(&buffer[..34]); + buffer[34] = (crc >> 8) as u8; + buffer[35] = (crc & 0xff) as u8; + + let mut output = [0u8; 48]; + if self.flags.base64_url { + BASE64_URL_SAFE + } else { + BASE64_STANDARD + } + .encode_slice(buffer, &mut output) + .unwrap(); + + // SAFETY: output is guaranteed to contain only ASCII. + let output = unsafe { std::str::from_utf8_unchecked(&output) }; + f.write_str(output) + } +} + /// Variable-length internal address. #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct VarAddr { @@ -812,4 +1009,83 @@ mod tests { }; assert_eq!(var_addr.prefix(), 0xb0bacafeb00b1e5a); } + + #[test] + fn base64_address() { + let addr = "0:84545d4d2cada0ce811705d534c298ca42d29315d03a16eee794cefd191dfa79" + .parse::() + .unwrap(); + assert_eq!( + addr.display_base64(true).to_string(), + "EQCEVF1NLK2gzoEXBdU0wpjKQtKTFdA6Fu7nlM79GR36eWpw" + ); + assert_eq!( + StdAddr::from_str_ext( + "EQCEVF1NLK2gzoEXBdU0wpjKQtKTFdA6Fu7nlM79GR36eWpw", + StdAddrFormat::any() + ) + .unwrap(), + ( + addr, + Base64StdAddrFlags { + testnet: false, + base64_url: false, + bounceable: true, + } + ) + ); + + let addr = "0:dddde93b1d3398f0b4305c08de9a032e0bc1b257c4ce2c72090aea1ff3e9ecfd" + .parse::() + .unwrap(); + assert_eq!( + addr.display_base64_url(false).to_string(), + "UQDd3ek7HTOY8LQwXAjemgMuC8GyV8TOLHIJCuof8-ns_Tyv" + ); + assert_eq!( + addr.display_base64(false).to_string(), + "UQDd3ek7HTOY8LQwXAjemgMuC8GyV8TOLHIJCuof8+ns/Tyv" + ); + + assert_eq!( + StdAddr::from_str_ext( + "UQDd3ek7HTOY8LQwXAjemgMuC8GyV8TOLHIJCuof8+ns/Tyv", + StdAddrFormat::any() + ) + .unwrap(), + ( + addr.clone(), + Base64StdAddrFlags { + testnet: false, + base64_url: false, + bounceable: false, + } + ) + ); + + assert_eq!( + addr.display_base64_url(true).to_string(), + "EQDd3ek7HTOY8LQwXAjemgMuC8GyV8TOLHIJCuof8-ns_WFq" + ); + assert_eq!( + addr.display_base64(true).to_string(), + "EQDd3ek7HTOY8LQwXAjemgMuC8GyV8TOLHIJCuof8+ns/WFq" + ); + + assert_eq!( + StdAddr::from_str_ext( + "EQDd3ek7HTOY8LQwXAjemgMuC8GyV8TOLHIJCuof8-ns_WFq", + StdAddrFormat::any() + ) + .unwrap(), + ( + addr, + Base64StdAddrFlags { + testnet: false, + base64_url: true, + bounceable: true, + } + ) + ); + } } diff --git a/src/util.rs b/src/util.rs index 9a72f067..20b80148 100644 --- a/src/util.rs +++ b/src/util.rs @@ -94,6 +94,42 @@ pub(crate) fn decode_base64_slice>( decode_base64_slice_impl(data.as_ref(), target) } +#[cfg(any(feature = "base64", test))] +#[inline] +pub(crate) fn crc_16(data: &[u8]) -> u16 { + let mut crc: u32 = 0; + for c in data { + let t = c ^ ((crc >> 8) as u8); + crc = (CRC16_TABLE[t as usize] ^ ((crc << 8) as u16)) as u32; + } + crc as u16 +} + +static CRC16_TABLE: [u16; 256] = [ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, + 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, + 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, + 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, + 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, + 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, + 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2, + 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, + 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, + 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, + 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, + 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, +]; + /// Small on-stack vector of max length N. pub struct ArrayVec { inner: [MaybeUninit; N],