From d45699f951e0b14cfc4d1433782906d677e5f8d3 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 3 Aug 2023 14:02:01 +1000 Subject: [PATCH] Add segwit API Add a segwit API with the aim that "typical" modern bitcoin usage is easy and correct. --- src/lib.rs | 1 + src/primitives/decode.rs | 58 ++------- src/primitives/mod.rs | 1 + src/primitives/segwit.rs | 98 +++++++++++++++ src/segwit.rs | 216 ++++++++++++++++++++++++++++++++++ tests/bip_173_test_vectors.rs | 13 ++ tests/bip_350_test_vectors.rs | 12 +- 7 files changed, 339 insertions(+), 60 deletions(-) create mode 100644 src/primitives/segwit.rs create mode 100644 src/segwit.rs diff --git a/src/lib.rs b/src/lib.rs index 4d775015f..605823199 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ pub use crate::primitives::{Bech32, Bech32m}; mod error; pub mod primitives; +pub mod segwit; #[cfg(feature = "arrayvec")] use arrayvec::{ArrayVec, CapacityError}; diff --git a/src/primitives/decode.rs b/src/primitives/decode.rs index e4dfa85c1..028c090fc 100644 --- a/src/primitives/decode.rs +++ b/src/primitives/decode.rs @@ -78,6 +78,7 @@ use crate::primitives::checksum::{self, Checksum}; use crate::primitives::gf32::Fe32; use crate::primitives::hrp::{self, Hrp}; use crate::primitives::iter::{Fe32IterExt, FesToBytes}; +use crate::primitives::segwit::{self, WitnessLengthError}; use crate::{write_err, Bech32, Bech32m}; /// Separator between the hrp and payload (as defined by BIP-173). @@ -264,7 +265,7 @@ impl<'s> CheckedHrpstring<'s> { self.data = &self.data[1..]; // Remove the witness version byte from data. self.validate_padding()?; - self.validate_witness_length(witness_version)?; + self.validate_witness_program_length(witness_version)?; Ok(SegwitHrpstring { hrp: self.hrp(), witness_version, data: self.data }) } @@ -309,21 +310,11 @@ impl<'s> CheckedHrpstring<'s> { /// Validates the segwit witness length rules. /// /// Must be called after the witness version byte is removed from the data. - #[allow(clippy::manual_range_contains)] // For witness length range check. - fn validate_witness_length(&self, witness_version: Fe32) -> Result<(), WitnessLengthError> { - use WitnessLengthError::*; - - let witness_len = self.byte_iter().len(); - if witness_len < 2 { - return Err(TooShort); - } - if witness_len > 40 { - return Err(TooLong); - } - if witness_version == Fe32::Q && witness_len != 20 && witness_len != 32 { - return Err(InvalidSegwitV0); - } - Ok(()) + fn validate_witness_program_length( + &self, + witness_version: Fe32, + ) -> Result<(), WitnessLengthError> { + segwit::validate_witness_program_length(self.byte_iter().len(), witness_version) } } @@ -746,41 +737,6 @@ impl std::error::Error for ChecksumError { } } -/// Witness program invalid because of incorrect length. -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub enum WitnessLengthError { - /// The witness data is too short. - TooShort, - /// The witness data is too long. - TooLong, - /// The segwit v0 witness is not 20 or 32 bytes long. - InvalidSegwitV0, -} - -impl fmt::Display for WitnessLengthError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use WitnessLengthError::*; - - match *self { - TooShort => write!(f, "witness program is less than 2 bytes long"), - TooLong => write!(f, "witness program is more than 40 bytes long"), - InvalidSegwitV0 => write!(f, "the segwit v0 witness is not 20 or 32 bytes long"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for WitnessLengthError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use WitnessLengthError::*; - - match *self { - TooShort | TooLong | InvalidSegwitV0 => None, - } - } -} - /// Error validating the padding bits on the witness data. #[derive(Debug, Clone, PartialEq, Eq)] pub enum PaddingError { diff --git a/src/primitives/mod.rs b/src/primitives/mod.rs index 478775a1a..1d496ba2b 100644 --- a/src/primitives/mod.rs +++ b/src/primitives/mod.rs @@ -8,6 +8,7 @@ pub mod encode; pub mod gf32; pub mod hrp; pub mod iter; +pub mod segwit; use checksum::{Checksum, PackedNull}; diff --git a/src/primitives/segwit.rs b/src/primitives/segwit.rs new file mode 100644 index 000000000..3b5b9750e --- /dev/null +++ b/src/primitives/segwit.rs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT + +//! Segregated Witness functionality - useful for enforcing parts of [`BIP-173`] and [`BIP-350`]. +//! +//! [BIP-173]: +//! [BIP-350]: + +use core::fmt; + +use crate::primitives::gf32::Fe32; + +/// Returns true if given field element represents a valid segwit version. +pub fn is_valid_witness_version(witness_version: Fe32) -> bool { + validate_witness_version(witness_version).is_ok() +} + +/// Returns true if `length` represents a valid witness program length for `witness_version`. +pub fn is_valid_witness_program_length(length: usize, witness_version: Fe32) -> bool { + validate_witness_program_length(length, witness_version).is_ok() +} + +/// Checks that the given field element represents a valid segwit witness version. +pub fn validate_witness_version(witness_version: Fe32) -> Result<(), InvalidWitnessVersionError> { + if witness_version.to_u8() > 16 { + Err(InvalidWitnessVersionError(witness_version)) + } else { + Ok(()) + } +} + +/// Validates the segwit witness program `length` rules for witness `version`. +pub fn validate_witness_program_length( + length: usize, + version: Fe32, +) -> Result<(), WitnessLengthError> { + use WitnessLengthError::*; + + if length < 2 { + return Err(TooShort); + } + if length > 40 { + return Err(TooLong); + } + if version == Fe32::Q && length != 20 && length != 32 { + return Err(InvalidSegwitV0); + } + Ok(()) +} + +/// Field element does not represent a valid witness version. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InvalidWitnessVersionError(Fe32); + +impl fmt::Display for InvalidWitnessVersionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "field element does not represent a valid witness version") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for InvalidWitnessVersionError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } +} + +/// Witness program invalid because of incorrect length. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum WitnessLengthError { + /// The witness data is too short. + TooShort, + /// The witness data is too long. + TooLong, + /// The segwit v0 witness is not 20 or 32 bytes long. + InvalidSegwitV0, +} + +impl fmt::Display for WitnessLengthError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use WitnessLengthError::*; + + match *self { + TooShort => write!(f, "witness program is less than 2 bytes long"), + TooLong => write!(f, "witness program is more than 40 bytes long"), + InvalidSegwitV0 => write!(f, "the segwit v0 witness is not 20 or 32 bytes long"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for WitnessLengthError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use WitnessLengthError::*; + + match *self { + TooShort | TooLong | InvalidSegwitV0 => None, + } + } +} diff --git a/src/segwit.rs b/src/segwit.rs new file mode 100644 index 000000000..0ee56cc48 --- /dev/null +++ b/src/segwit.rs @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MIT + +//! Segregated Witness API - enabling typical usage for encoding and decoding segwit addresses. +//! +//! The bips contain some complexity, this module should allow you to create modern bitcoin +//! addresses, and parse existing addresses from the Bitcoin blockchain, correctly and easily. +//! +//! You should not need to intimately know [BIP-173] and [BIP-350] in order to correctly use this +//! module. If you are doing unusual things, consider using the `primitives` submodules directly. +//! +//! Note, we do implement the bips to spec, however this is done in the `primitives` submodules, to +//! convince yourself, and to see the nitty gritty, you can look at the test vector code in +//! [`bip_173_test_vectors.rs`] and [`bip_350_test_vectors.rs`]. +//! +//! # Examples +//! +//! ``` +//! # #[cfg(feature = "alloc")] { +//! use bech32::primitives::hrp::{self, Hrp}; +//! use bech32::{Fe32, segwit}; +//! +//! let witness_prog = [ +//! 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, +//! 0x54, 0x94, 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23, +//! 0xf1, 0x43, 0x3b, 0xd6, +//! ]; +//! +//! // Encode a taproot address suitable for use on mainnet. +//! let _ = segwit::encode_v0(&hrp::BC, &witness_prog); +//! let _ = segwit::encode(&hrp::BC, Fe32::P, &witness_prog); +//! +//! // Encode a segwit v0 address suitable for use on testnet. +//! let s = segwit::encode(&hrp::TB, Fe32::Q, &witness_prog); +//! +//! // Decode a segwit v0 address suitable for use on testnet. +//! let s = segwit::encode(&hrp::TB, Fe32::Q, &witness_prog); +//! # } +//! ``` +//! +//! [BIP-173]: +//! [BIP-350]: +//! [`bip_173_test_vectors.rs`]: +//! [`bip_350_test_vectors.rs`]: + +#[cfg(all(feature = "alloc", not(feature = "std"), not(test)))] +use alloc::{string::String, vec::Vec}; +use core::fmt; + +#[cfg(feature = "alloc")] +use crate::primitives::decode::{SegwitHrpstring, SegwitHrpstringError}; +use crate::primitives::gf32::Fe32; +// TODO: Add ergonomic re-exports of types used in this modules public API, ether to `lib.rs` or here. +use crate::primitives::hrp::Hrp; +use crate::primitives::iter::{ByteIterExt, Fe32IterExt}; +#[cfg(feature = "alloc")] +use crate::primitives::segwit; +use crate::primitives::segwit::{InvalidWitnessVersionError, WitnessLengthError}; +use crate::primitives::{Bech32, Bech32m}; +use crate::write_err; + +/// Decodes a segwit address. +#[cfg(feature = "alloc")] +pub fn decode(s: &str) -> Result<(Hrp, Fe32, Vec), SegwitHrpstringError> { + let segwit = SegwitHrpstring::new(s)?; + Ok((segwit.hrp(), segwit.witness_version(), segwit.byte_iter().collect::>())) +} + +/// Encodes a segwit address. +/// +/// Does validity checks on the `witness_version` and length checks on the `witness_program`. +/// +/// As specified by [`BIP-350`] we use the [`Bech32m`] checksum algorithm for witness versions 1 and +/// above, and for witness version 0 we use the original ([`BIP-173`]) [`Bech32`] checksum +/// algorithm. +/// +/// You may prefer to use [`encode_v0`] or [`encode_v1`]. +/// +/// [`Bech32`]: crate::primitives::Bech32 +/// [`Bech32m`]: crate::primitives::Bech32m +/// [BIP-173]: +/// [BIP-350]: +#[cfg(feature = "alloc")] +pub fn encode( + hrp: &Hrp, + witness_version: Fe32, + witness_program: &[u8], +) -> Result { + segwit::validate_witness_version(witness_version)?; + segwit::validate_witness_program_length(witness_program.len(), witness_version)?; + + let mut buf = String::new(); + encode_to_fmt_unchecked(&mut buf, hrp, witness_version, witness_program)?; + Ok(buf) +} + +/// Encodes a segwit version 0 address. +#[cfg(feature = "alloc")] +pub fn encode_v0(hrp: &Hrp, witness_program: &[u8]) -> Result { + encode(hrp, Fe32::Q, witness_program) +} + +/// Encodes a segwit version 1 address. +#[cfg(feature = "alloc")] +pub fn encode_v1(hrp: &Hrp, witness_program: &[u8]) -> Result { + encode(hrp, Fe32::P, witness_program) +} + +/// Encodes a segwit address to a writer ([`fmt::Write`]). +/// +/// Does not check the validity of the witness version and witness program lengths (see +/// the [`crate::primitives::segwit`] module for validation functions). +pub fn encode_to_fmt_unchecked( + fmt: &mut dyn fmt::Write, + hrp: &Hrp, + witness_version: Fe32, + witness_program: &[u8], +) -> fmt::Result { + match witness_version { + Fe32::Q => { + for c in witness_program + .iter() + .copied() + .bytes_to_fes() + .with_checksum::(hrp) + .with_witness_version(Fe32::Q) + .chars() + { + fmt.write_char(c)?; + } + } + version => { + for c in witness_program + .iter() + .copied() + .bytes_to_fes() + .with_checksum::(hrp) + .with_witness_version(version) + .chars() + { + fmt.write_char(c)?; + } + } + } + + Ok(()) +} + +/// An error while constructing a [`SegwitHrpstring`] type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EncodeError { + /// Invalid witness version (must be 0-16 inclusive). + WitnessVersion(InvalidWitnessVersionError), + /// Invalid witness length. + WitnessLength(WitnessLengthError), + /// Writing to formatter failed. + Write(fmt::Error), +} + +impl fmt::Display for EncodeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use EncodeError::*; + + match *self { + WitnessVersion(ref e) => write_err!(f, "witness version"; e), + WitnessLength(ref e) => write_err!(f, "witness length"; e), + Write(ref e) => write_err!(f, "writing to formatter failed"; e), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for EncodeError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use EncodeError::*; + + match *self { + WitnessVersion(ref e) => Some(e), + WitnessLength(ref e) => Some(e), + Write(ref e) => Some(e), + } + } +} + +impl From for EncodeError { + fn from(e: InvalidWitnessVersionError) -> Self { Self::WitnessVersion(e) } +} + +impl From for EncodeError { + fn from(e: WitnessLengthError) -> Self { Self::WitnessLength(e) } +} + +impl From for EncodeError { + fn from(e: fmt::Error) -> Self { Self::Write(e) } +} + +#[cfg(all(test, feature = "alloc"))] +mod tests { + use super::*; + + #[test] + // Just shows we handle both v0 and v1 addresses, for complete test + // coverage see primitives submodules and test vectors. + fn roundtrip_valid_mainnet_addresses() { + // A few recent addresses from mainnet (Block 801266). + let addresses = vec![ + "bc1q2s3rjwvam9dt2ftt4sqxqjf3twav0gdx0k0q2etxflx38c3x8tnssdmnjq", // Segwit v0 + "bc1py3m7vwnghyne9gnvcjw82j7gqt2rafgdmlmwmqnn3hvcmdm09rjqcgrtxs", // Segwit v1 + ]; + + for address in addresses { + let (hrp, version, program) = decode(address).expect("failed to decode valid address"); + let encoded = encode(&hrp, version, &program).expect("failed to encode address"); + assert_eq!(encoded, address); + } + } +} diff --git a/tests/bip_173_test_vectors.rs b/tests/bip_173_test_vectors.rs index bb645cf60..8ee7b1217 100644 --- a/tests/bip_173_test_vectors.rs +++ b/tests/bip_173_test_vectors.rs @@ -55,6 +55,19 @@ macro_rules! check_valid_address_roundtrip { $( #[test] fn $test_name() { + // We cannot use encode/decode for all test vectors because according to BIP-173 the + // bech32 checksum algorithm can be used with any witness version, and this is + // tested by the test vectors. However when BIP-350 came into effect only witness + // version 0 uses bech32 (and this is enforced by encode/decode). + if let Ok((hrp, bech32::Fe32::Q, program)) = bech32::segwit::decode($addr) { + let encoded = bech32::segwit::encode_v0(&hrp, &program).expect("failed to encode address"); + // The bips specifically say that encoder should output lowercase characters so we uppercase manually. + if encoded != $addr { + let got = encoded.to_uppercase(); + assert_eq!(got, $addr) + } + } + let hrpstring = SegwitHrpstring::new_bech32($addr).expect("valid address"); let hrp = hrpstring.hrp(); let witness_version = hrpstring.witness_version(); diff --git a/tests/bip_350_test_vectors.rs b/tests/bip_350_test_vectors.rs index fb5d870cc..8f41d24e2 100644 --- a/tests/bip_350_test_vectors.rs +++ b/tests/bip_350_test_vectors.rs @@ -6,7 +6,7 @@ use bech32::primitives::decode::{ CheckedHrpstring, CheckedHrpstringError, ChecksumError, SegwitHrpstring, SegwitHrpstringError, UncheckedHrpstring, }; -use bech32::{Bech32, Bech32m, ByteIterExt, Fe32, Fe32IterExt}; +use bech32::{Bech32, Bech32m}; // This is a separate test because we correctly identify this string as invalid but not for the // reason given in the bip. @@ -55,14 +55,8 @@ macro_rules! check_valid_address_roundtrip { #[test] #[cfg(feature = "alloc")] fn $test_name() { - let hrpstring = SegwitHrpstring::new($addr).expect("valid address"); - let hrp = hrpstring.hrp(); - let witness_version = hrpstring.witness_version(); - - let encoded = match witness_version { - Fe32::Q => hrpstring.byte_iter().bytes_to_fes().with_checksum::(&hrp.into()).with_witness_version(witness_version).chars().collect::(), - _ => hrpstring.byte_iter().bytes_to_fes().with_checksum::(&hrp.into()).with_witness_version(witness_version).chars().collect::(), - }; + let (hrp, version, program) = bech32::segwit::decode($addr).expect("failed to decode valid address"); + let encoded = bech32::segwit::encode(&hrp, version, &program).expect("failed to encode address"); // The bips specifically say that encoder should output lowercase characters so we uppercase manually. if encoded != $addr {