Skip to main content

spark_address/
lib.rs

1//! Minimal Spark address codec.
2//! Avoids unsafe code and works in `no_std` + `alloc` environments.
3//!
4//! # Crate Overview
5//! The **spark-address** crate encodes & decodes *Spark* Bech32m addresses. A Spark
6//! address couples a compressed secp256k1 public key with a network identifier
7//! (see [`Network`]) and represents them as human-friendly Bech32m strings like
8//! `sp1…` or `sprt1…`.
9//!
10//! ```rust
11//! use spark_address::{encode_spark_address, decode_spark_address, SparkAddressData, Network};
12//!
13//! let pubkey = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
14//! let data = SparkAddressData { identity_public_key: pubkey.into(), network: Network::Mainnet };
15//! let addr = encode_spark_address(&data)?;
16//! let decoded = decode_spark_address(&addr)?;
17//! assert_eq!(decoded, data);
18//! # Ok::<(), spark_address::SparkAddressError>(())
19//! ```
20//!
21//! ## Feature Flags
22//! * **`std`** *(default)* — Use the Rust standard library. Disable to build for
23//!   `#![no_std]` + `alloc` targets.
24//! * **`validate-secp256k1`** — Validate the public key using the `secp256k1` crate.
25//!
26//! ## MSRV
27//! Minimum supported Rust version: **1.70**.
28
29#![forbid(unsafe_code)]
30#![cfg_attr(not(feature = "std"), no_std)]
31
32extern crate alloc;
33use alloc::string::ToString;
34use bech32::{self, Bech32m, Hrp};
35use core::fmt;
36use hex::{decode as hex_to_bytes, encode as bytes_to_hex};
37
38// `Vec` / `String` come from `alloc` when `std` is disabled.
39#[cfg(not(feature = "std"))]
40use alloc::{string::String, vec::Vec};
41
42#[cfg(feature = "std")]
43use std::{string::String, vec::Vec};
44
45/* ------------------------------------------------------------- *
46 *  Network ⇄ HRP                                                 *
47 * ------------------------------------------------------------- */
48
49/// Networks supported by Spark.
50#[derive(Debug, Copy, Clone, Eq, PartialEq)]
51pub enum Network {
52    /// Main Bitcoin network (`sp` prefix).
53    Mainnet,
54    /// Bitcoin testnet (`spt` prefix).
55    Testnet,
56    /// Signet (`sps` prefix).
57    Signet,
58    /// Regression-test network (`sprt` prefix).
59    Regtest,
60    /// Local development network (`spl` prefix).
61    Local,
62}
63
64impl Network {
65    fn hrp(self) -> &'static str {
66        match self {
67            Network::Mainnet => "sp",
68            Network::Testnet => "spt",
69            Network::Signet => "sps",
70            Network::Regtest => "sprt",
71            Network::Local => "spl",
72        }
73    }
74
75    fn from_hrp(hrp: &str) -> Option<Self> {
76        match hrp {
77            "sp" => Some(Network::Mainnet),
78            "spt" => Some(Network::Testnet),
79            "sps" => Some(Network::Signet),
80            "sprt" => Some(Network::Regtest),
81            "spl" => Some(Network::Local),
82            _ => None,
83        }
84    }
85}
86
87/* ------------------------------------------------------------- *
88 *  Error type                                                    *
89 * ------------------------------------------------------------- */
90
91#[derive(Debug)]
92pub enum SparkAddressError {
93    /// The Bech32 string failed to decode.
94    InvalidBech32(bech32::DecodeError),
95    /// The human-readable part (HRP) does not correspond to a known [`Network`].
96    UnknownPrefix(String),
97    /// The checksum was valid **Bech32** but not **Bech32m**.
98    InvalidVariant,
99    /// The string mixes upper- and lower-case characters.
100    MixedCase,
101    /// The address exceeded the 90-character limit specified by BIP-350.
102    InvalidLength,
103    /// The embedded pseudo-protobuf payload was malformed.
104    BadProto,
105    /// Public key hex failed to decode.
106    Hex(hex::FromHexError),
107    /// Public key length differed from 33 bytes.
108    WrongKeyLength(usize),
109    #[cfg(feature = "validate-secp256k1")]
110    /// The provided public key is not a valid compressed secp256k1 key.
111    InvalidSecp256k1,
112    /// Failure while encoding back into Bech32m.
113    Bech32Encode(bech32::EncodeError),
114}
115
116impl fmt::Display for SparkAddressError {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        match self {
119            SparkAddressError::InvalidBech32(e) => write!(f, "bech32 decode error: {e}"),
120            SparkAddressError::UnknownPrefix(p) => write!(f, "unknown HRP prefix: {p}"),
121            SparkAddressError::InvalidVariant => write!(f, "bech32 variant is not Bech32m"),
122            SparkAddressError::MixedCase => write!(f, "address contains mixed upper/lower case"),
123            SparkAddressError::InvalidLength => write!(f, "address exceeds maximum length (90)"),
124            SparkAddressError::BadProto => write!(f, "invalid proto payload"),
125            SparkAddressError::Hex(e) => write!(f, "hex decode error: {e}"),
126            SparkAddressError::WrongKeyLength(n) => {
127                write!(f, "wrong pubkey length: {n} (expected 33)")
128            }
129            #[cfg(feature = "validate-secp256k1")]
130            SparkAddressError::InvalidSecp256k1 => write!(f, "invalid secp256k1 pubkey"),
131            SparkAddressError::Bech32Encode(e) => write!(f, "bech32 encode error: {e}"),
132        }
133    }
134}
135
136#[cfg(feature = "std")]
137impl std::error::Error for SparkAddressError {}
138
139impl From<bech32::DecodeError> for SparkAddressError {
140    fn from(e: bech32::DecodeError) -> Self {
141        Self::InvalidBech32(e)
142    }
143}
144
145impl From<bech32::EncodeError> for SparkAddressError {
146    fn from(e: bech32::EncodeError) -> Self {
147        Self::Bech32Encode(e)
148    }
149}
150
151impl From<hex::FromHexError> for SparkAddressError {
152    fn from(e: hex::FromHexError) -> Self {
153        Self::Hex(e)
154    }
155}
156
157/* ------------------------------------------------------------- *
158 *  SparkAddressData                                              *
159 * ------------------------------------------------------------- */
160
161/// Result of a successful decode, or input to `encode`.
162#[derive(Debug, Clone, Eq, PartialEq)]
163pub struct SparkAddressData {
164    /// Compressed secp256k1 public key, hex-encoded (`02/03 + 32 bytes`).
165    pub identity_public_key: String,
166    /// Network for which the address is intended (determines HRP prefix).
167    pub network: Network,
168}
169
170/* ------------------------------------------------------------- *
171 *  Tiny "proto" wrapper (field-1, wire-type 2)                   *
172 * ------------------------------------------------------------- */
173
174const TAG: u8 = 0x0a; // (1 << 3) | 2
175
176fn encode_proto(key: &[u8]) -> Vec<u8> {
177    let mut out = Vec::with_capacity(2 + key.len());
178    out.push(TAG);
179    // Compressed pubkeys are 33 bytes; fall back to error if ever larger.
180    let key_len: u8 = key.len().try_into().expect("key length exceeds 255 bytes");
181    out.push(key_len);
182    out.extend_from_slice(key);
183    out
184}
185
186fn decode_proto(buf: &[u8]) -> Result<&[u8], SparkAddressError> {
187    if buf.len() >= 3 && buf[0] == TAG && buf[1] as usize + 2 == buf.len() {
188        Ok(&buf[2..])
189    } else {
190        Err(SparkAddressError::BadProto)
191    }
192}
193
194/* ------------------------------------------------------------- *
195 *  Public API                                                    *
196 * ------------------------------------------------------------- */
197
198/// Encode a `(pubkey, network)` into a Spark Bech32m address.
199///
200/// # Panics
201///
202/// This function will panic if the HRP (Human Readable Part) is invalid. This should never happen
203/// in practice as the HRP is statically defined in the `Network` enum.
204///
205/// # Errors
206///
207/// This function will return an error if:
208/// * The public key is invalid hex (`SparkAddressError::Hex`)
209/// * The public key length is not 33 bytes (`SparkAddressError::WrongKeyLength`)
210/// * The public key is invalid secp256k1 (when `validate-secp256k1` feature is enabled) (`SparkAddressError::InvalidSecp256k1`)
211/// * The bech32 encoding fails (`SparkAddressError::Bech32Encode`)
212pub fn encode_spark_address(data: &SparkAddressData) -> Result<String, SparkAddressError> {
213    #[cfg(feature = "validate-secp256k1")]
214    validate_pubkey(&data.identity_public_key)?;
215
216    let key_bytes = hex_to_bytes(&data.identity_public_key)?;
217    if key_bytes.len() != 33 {
218        return Err(SparkAddressError::WrongKeyLength(key_bytes.len()));
219    }
220
221    let proto = encode_proto(&key_bytes);
222
223    let hrp = Hrp::parse(data.network.hrp()).expect("static HRP is valid");
224    let addr = bech32::encode::<Bech32m>(hrp, &proto)?;
225
226    Ok(addr)
227}
228
229/// Decode a Spark address, returning `(pubkey, network)`.
230///
231/// # Errors
232///
233/// This function will return an error if:
234/// * The address is not valid bech32m (`SparkAddressError::InvalidBech32`)
235/// * The address has an unknown prefix (`SparkAddressError::UnknownPrefix`)
236/// * The address has invalid protocol data (`SparkAddressError::BadProto`)
237/// * The public key length is not 33 bytes (`SparkAddressError::WrongKeyLength`)
238/// * The public key is invalid secp256k1 (when `validate-secp256k1` feature is enabled) (`SparkAddressError::InvalidSecp256k1`)
239pub fn decode_spark_address(addr: &str) -> Result<SparkAddressData, SparkAddressError> {
240    // -----------------------------------------------------------------
241    // Early sanity checks (avoid allocating in `bech32::decode` when we
242    // already know the string is invalid).
243    // -----------------------------------------------------------------
244    if addr.len() > 90 {
245        return Err(SparkAddressError::InvalidLength);
246    }
247
248    let has_upper = addr.bytes().any(|b| b.is_ascii_uppercase());
249    let has_lower = addr.bytes().any(|b| b.is_ascii_lowercase());
250    if has_upper && has_lower {
251        return Err(SparkAddressError::MixedCase);
252    }
253
254    let (hrp, proto) = bech32::decode(addr)?;
255
256    // The Bech32 spec requires the HRP to be lowercase. The `bech32`
257    // crate accepts uppercase HRPs, so we enforce the stricter rule
258    // here.
259    let hrp_str = hrp.to_string();
260    if hrp_str.bytes().any(|b| b.is_ascii_uppercase()) {
261        return Err(SparkAddressError::MixedCase);
262    }
263
264    // Reject legacy Bech32 (BIP-173) by re-encoding with Bech32m and
265    // comparing the checksum. If it differs, the original variant must
266    // have been classic Bech32.
267    let reencoded = bech32::encode::<Bech32m>(hrp, &proto)?;
268    if reencoded.to_lowercase() != addr.to_lowercase() {
269        return Err(SparkAddressError::InvalidVariant);
270    }
271
272    let network = Network::from_hrp(&hrp_str)
273        .ok_or_else(|| SparkAddressError::UnknownPrefix(hrp_str.clone()))?;
274
275    let key = decode_proto(&proto)?;
276
277    if key.len() != 33 {
278        return Err(SparkAddressError::WrongKeyLength(key.len()));
279    }
280
281    let hex_key = bytes_to_hex(key);
282
283    #[cfg(feature = "validate-secp256k1")]
284    validate_pubkey(&hex_key)?;
285
286    Ok(SparkAddressData {
287        identity_public_key: hex_key,
288        network,
289    })
290}
291
292/* ------------------------------------------------------------- *
293 *  (feature-gated) secp256k1 validation                               *
294 * ------------------------------------------------------------- */
295
296#[cfg(feature = "validate-secp256k1")]
297fn validate_pubkey(hex_str: &str) -> Result<(), SparkAddressError> {
298    use secp256k1::PublicKey;
299    let bytes = hex_to_bytes(hex_str)?;
300    PublicKey::from_slice(&bytes).map_err(|_| SparkAddressError::InvalidSecp256k1)?;
301    Ok(())
302}
303
304#[cfg(not(feature = "validate-secp256k1"))]
305fn _validate_pubkey(_: &str) {}
306
307/* ------------------------------------------------------------- *
308 *  Tests                                                         *
309 * ------------------------------------------------------------- */
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    const PUBKEY: &str = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
316    const MAINNET_ADDRESS: &str =
317        "sp1pgssy7d7vel0nh9m4326qc54e6rskpczn07dktww9rv4nu5ptvt0s9ucez8h3s";
318    const REGTEST_ADDRESS: &str =
319        "sprt1pgssy7d7vel0nh9m4326qc54e6rskpczn07dktww9rv4nu5ptvt0s9ucd5rgc0";
320
321    #[test]
322    fn mainnet_round_trip() {
323        let data = SparkAddressData {
324            identity_public_key: PUBKEY.into(),
325            network: Network::Mainnet,
326        };
327        let encoded = encode_spark_address(&data).unwrap();
328        assert_eq!(encoded, MAINNET_ADDRESS);
329        let decoded = decode_spark_address(&encoded).unwrap();
330        assert_eq!(decoded, data);
331
332        let decoded = decode_spark_address(MAINNET_ADDRESS).unwrap();
333        assert_eq!(decoded.network, Network::Mainnet);
334        assert_eq!(decoded.identity_public_key, PUBKEY);
335    }
336
337    #[test]
338    fn regtest_round_trip() {
339        let data = SparkAddressData {
340            identity_public_key: PUBKEY.into(),
341            network: Network::Regtest,
342        };
343        let encoded = encode_spark_address(&data).unwrap();
344        assert_eq!(encoded, REGTEST_ADDRESS);
345        let decoded = decode_spark_address(&encoded).unwrap();
346        assert_eq!(decoded, data);
347
348        let decoded = decode_spark_address(MAINNET_ADDRESS).unwrap();
349        assert_eq!(decoded.network, Network::Mainnet);
350        assert_eq!(decoded.identity_public_key, PUBKEY);
351    }
352}