Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,17 @@ rust_decimal = "1"
bech32 = "0.11"
bellman = "0.14"
blake2b_simd = "1"
bs58 = "0.5"
ed25519-zebra = "4"
equihash = "0.2"
group = "0.13"
lazy_static = "1"
phf = "0.11"
regex = "1"
secp256k1 = "0.29"
serde_json = "1"
sha2 = "0.10"
sha3 = "0.10"
uint = "0.9"
zcash_encoding = "0.3"
zcash_note_encryption = "0.4.1"
Expand Down
3 changes: 3 additions & 0 deletions src/commands/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use context::{Context, ZUint256};

pub(crate) mod address;
pub(crate) mod block;
pub(crate) mod foreign_address;
pub(crate) mod keys;
pub(crate) mod lookup;
pub(crate) mod transaction;
Expand Down Expand Up @@ -64,6 +65,8 @@ impl Command {
inspect_bytes(bytes, opts.context, opts.lookup).await;
} else if let Ok(addr) = ZcashAddress::try_from_encoded(&opts.data) {
address::inspect(addr);
} else if let Some(addr) = foreign_address::detect(&opts.data) {
foreign_address::inspect(addr);
} else if let Ok((network, uivk)) = unified::Uivk::decode(&opts.data) {
keys::view::inspect_uivk(uivk, network);
} else if let Ok((network, ufvk)) = unified::Ufvk::decode(&opts.data) {
Expand Down
162 changes: 162 additions & 0 deletions src/commands/inspect/foreign_address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
use bech32::{primitives::decode::CheckedHrpstring, Bech32, Bech32m};
use phf::phf_map;
use regex::Regex;
use sha2::Digest;
use sha3::Keccak256;

pub(crate) fn detect(s: &str) -> Option<ForeignAddress> {
// Remove leading and trailing whitespace, to handle copy-paste errors.
let s = s.trim();

// Try decoding as Bech32.
if let Ok(parsed) = CheckedHrpstring::new::<Bech32>(s) {
// If we reached this point, the encoding is found to be valid Bech32.
if let Some(&token_class) = BECH32_HRP.get(parsed.hrp().as_str()) {
return Some(ForeignAddress { token_class });
}
}

// Try decoding as Bech32m.
if let Ok(parsed) = CheckedHrpstring::new::<Bech32m>(s) {
// If we reached this point, the encoding is found to be valid Bech32m.
if let Some(&token_class) = BECH32M_HRP.get(parsed.hrp().as_str()) {
return Some(ForeignAddress { token_class });
}
}

// Try decoding as Base58Check.
if let Ok(decoded) = bs58::decode(s).with_check(None).into_vec() {
if !decoded.is_empty() {
if let Some(&token_class) = B58CHECK_BITCOIN_PREFIX_1.get(&decoded[..1]) {
return Some(ForeignAddress { token_class });
}
}
};

// Try decoding as Base58Check.
if let Ok(decoded) = bs58::decode(s)
.with_alphabet(bs58::Alphabet::RIPPLE)
.with_check(None)
.into_vec()
{
if !decoded.is_empty() {
if let Some(&token_class) = B58CHECK_RIPPLE_PREFIX_1.get(&decoded[..1]) {
return Some(ForeignAddress { token_class });
}
}
};

// Try decoding as an Ethereum address.
if Regex::new(r"^0x[0-9a-fA-F]{40}$")
.expect("valid")
.is_match(s)
{
// If it's all lowercase or all uppercase then this is all we can validate.
if Regex::new(r"^0x[0-9a-f]{40}$").expect("valid").is_match(s)
|| Regex::new(r"^0x[0-9A-F]{40}$").expect("valid").is_match(s)
{
return Some(ForeignAddress {
token_class: TokenClass::EvmFork,
});
}

// Otherwise try decoding as an ERC-55 address.
let hex_chars = &s[2..];
let hashed_address = hex::encode(Keccak256::digest(hex_chars.to_ascii_lowercase()));
if hex_chars.chars().enumerate().all(|(i, c)| {
c.is_ascii_digit()
|| (u8::from_str_radix(&hashed_address[i..i + 1], 16).unwrap() >= 8)
== c.is_ascii_uppercase()
}) {
return Some(ForeignAddress {
token_class: TokenClass::EvmFork,
});
}
}

// If we reach here, we didn't detect anything.
None
}

pub(crate) fn inspect(addr: ForeignAddress) {
eprintln!("Not a Zcash address");
eprintln!();
eprintln!("Token class: {:?}", addr.token_class);
}

/// Information about a non-Zcash (foreign) address.
#[derive(Debug)]
pub(crate) struct ForeignAddress {
token_class: TokenClass,
}

/// The class of token that can be sent to a given address.
#[derive(Clone, Copy, Debug)]
enum TokenClass {
/// Bitcoin, and any forks of it that persist.
Bitcoin,
/// Testnet Bitcoin, as well as many many code forks that didn't change testnet
/// prefixes.
TestnetBitcoin,
/// Regtest Bitcoin, not relevant outside Bitcoin Core codebase.
RegtestBitcoin,

/// Ethereum (which has a frustrating address format), or any of its many many chain
/// or code forks (which did not alter it). The networks that can use these addresses
/// [include]:
///
/// - Ethereum
/// - Polygon
/// - BSC (BNB Chain)
/// - Fantom
/// - Avalanche (C-Chain)
///
/// [include]: https://support.metamask.io/start/learn/the-ethereum-address-format-and-why-it-matters-when-using-metamask/
EvmFork,

/// Tokens on the XRP Ledger.
XrpLedger,
}

/// A map from one-byte Base58Check prefixes decoded using the Bitcoin alphabet, to the
/// token class that the address can receive.
///
/// - https://en.bitcoin.it/wiki/List_of_address_prefixes
static B58CHECK_BITCOIN_PREFIX_1: phf::Map<[u8; 1], TokenClass> = phf_map! {
[0x00] => TokenClass::Bitcoin,
[0x05] => TokenClass::Bitcoin,
[0x6f] => TokenClass::TestnetBitcoin,
[0xc4] => TokenClass::TestnetBitcoin,
};

/// A map from one-byte Base58Check prefixes decoded using the Ripple alphabet, to the
/// token class that the address can receive.
///
/// - https://xrpl.org/docs/references/protocol/data-types/base58-encodings
static B58CHECK_RIPPLE_PREFIX_1: phf::Map<[u8; 1], TokenClass> = phf_map! {
[0x00] => TokenClass::XrpLedger,
};

/// A map from Bech32 HRPs to the token class that the address can receive.
///
/// [SLIP-0173] has a big list of these, but they need to be individually mapped to token
/// classes.
///
/// [SLIP-0173]: https://github.com/satoshilabs/slips/blob/master/slip-0173.md
static BECH32_HRP: phf::Map<&'static str, TokenClass> = phf_map! {
"bc" => TokenClass::Bitcoin,
"bcrt" => TokenClass::RegtestBitcoin,
"tb" => TokenClass::TestnetBitcoin,
};

/// A map from Bech32m HRPs to the token class that the address can receive.
///
/// [SLIP-0173] HRPs might happen to also be Bech32m HRPs (this is what Bitcoin did, and
/// thus anyone who forked their code), but this should be checked for each case.
///
/// [SLIP-0173]: https://github.com/satoshilabs/slips/blob/master/slip-0173.md
static BECH32M_HRP: phf::Map<&'static str, TokenClass> = phf_map! {
"bc" => TokenClass::Bitcoin,
"bcrt" => TokenClass::RegtestBitcoin,
"tb" => TokenClass::TestnetBitcoin,
};
Loading