From 290f56dc05438cfdde795ab455565fc0a50cd244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Olav?= Date: Mon, 31 Aug 2026 10:19:18 +0200 Subject: [PATCH] types: read the deposit address a deposit carries --- lib/types/proto.rs | 6 +++- types/address.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++ types/error.rs | 6 ++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/lib/types/proto.rs b/lib/types/proto.rs index d35d4356..d1dad324 100644 --- a/lib/types/proto.rs +++ b/lib/types/proto.rs @@ -480,7 +480,11 @@ pub mod mainchain { break 'address Address::ALL_ZEROS; } }; - match Address::from_str(address_utf8) { + // A deposit carries the prefixed form. A bare address also + // parses. + match Address::from_deposit_address(address_utf8) + .or_else(|_| Address::from_str(address_utf8)) + { Ok(address) => address, Err(_) => { tracing::warn!( diff --git a/types/address.rs b/types/address.rs index cbc954b0..5f0f1d6f 100644 --- a/types/address.rs +++ b/types/address.rs @@ -26,6 +26,31 @@ impl Address { sha256::Hash::hash(prefix.as_bytes()).to_byte_array(); format!("{prefix}{}", const_hex::encode(&prefix_digest[..3])) } + + /// Parse the form that `format_for_deposit` writes + pub fn from_deposit_address(s: &str) -> Result { + let prefix = format!("s{THIS_SIDECHAIN}_"); + let rest = s.strip_prefix(&prefix).ok_or_else(|| { + ParseAddressError::MissingDepositPrefix(s.to_owned()) + })?; + let (address_str, checksum) = rest + .rsplit_once('_') + .filter(|(_, checksum)| !checksum.is_empty()) + .ok_or_else(|| { + ParseAddressError::MissingDepositChecksum(s.to_owned()) + })?; + let digest = + sha256::Hash::hash(format!("{prefix}{address_str}_").as_bytes()) + .to_byte_array(); + // A writer may use a longer checksum, so compare only what it names. + if !const_hex::encode(digest).starts_with(&checksum.to_lowercase()) { + return Err(ParseAddressError::WrongDepositChecksum { + address: s.to_owned(), + checksum: checksum.to_owned(), + }); + } + address_str.parse() + } } impl std::fmt::Display for Address { @@ -81,3 +106,48 @@ impl Serialize for Address { } } } + +#[cfg(test)] +mod test { + use bitcoin::hashes::{Hash as _, sha256}; + + use crate::{THIS_SIDECHAIN, address::Address}; + + #[test] + fn deposit_address_round_trip() { + let address = Address([7u8; 20]); + let formatted = address.format_for_deposit(); + assert_eq!(Address::from_deposit_address(&formatted).unwrap(), address); + } + + #[test] + fn deposit_address_accepts_longer_checksum() { + let address = Address([9u8; 20]); + let prefix = format!("s{}_{}_", THIS_SIDECHAIN, address.as_base58()); + let digest = sha256::Hash::hash(prefix.as_bytes()).to_byte_array(); + let formatted = format!("{prefix}{}", const_hex::encode(&digest[..6])); + assert_eq!(Address::from_deposit_address(&formatted).unwrap(), address); + } + + #[test] + fn deposit_address_rejects_wrong_checksum() { + let address = Address([3u8; 20]); + let formatted = + format!("s{}_{}_ffffff", THIS_SIDECHAIN, address.as_base58()); + assert!(Address::from_deposit_address(&formatted).is_err()); + } + + #[test] + fn deposit_address_rejects_wrong_sidechain() { + let address = Address([3u8; 20]); + let formatted = + format!("s{}_{}_000000", THIS_SIDECHAIN + 1, address.as_base58()); + assert!(Address::from_deposit_address(&formatted).is_err()); + } + + #[test] + fn deposit_address_rejects_bare_address() { + let address = Address([3u8; 20]); + assert!(Address::from_deposit_address(&address.as_base58()).is_err()); + } +} diff --git a/types/error.rs b/types/error.rs index d7566c69..41b120b5 100644 --- a/types/error.rs +++ b/types/error.rs @@ -42,6 +42,12 @@ pub enum ComputeFee { pub enum ParseAddress { #[error("bs58 error")] Bs58(#[from] bitcoin::base58::InvalidCharacterError), + #[error("deposit address `{0}` has no checksum")] + MissingDepositChecksum(String), + #[error("deposit address `{0}` has no `s_` prefix")] + MissingDepositPrefix(String), + #[error("deposit address `{address}` has wrong checksum `{checksum}`")] + WrongDepositChecksum { address: String, checksum: String }, #[error("wrong address length {0} != 20")] WrongLength(usize), }