Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Taproot P2TR address #563

Merged
merged 3 commits into from Sep 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 41 additions & 9 deletions src/util/address.rs
Expand Up @@ -40,6 +40,7 @@ use core::num::ParseIntError;
use core::str::FromStr;
#[cfg(feature = "std")] use std::error;

use secp256k1::schnorrsig;
use bech32;
use hashes::Hash;
use hash_types::{PubkeyHash, WPubkeyHash, ScriptHash, WScriptHash};
Expand All @@ -59,6 +60,13 @@ pub enum Error {
Bech32(bech32::Error),
/// The bech32 payload was empty
EmptyBech32Payload,
/// The wrong checksum algorithm was used. See BIP-0350.
InvalidBech32Variant {
/// Bech32 variant that is required by the used Witness version
expected: bech32::Variant,
/// The actual Bech32 variant encoded in the address representation
found: bech32::Variant
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome error reporting here!

/// Script version must be 0 to 16 inclusive
InvalidWitnessVersion(u8),
/// Unable to parse witness version from string
Expand All @@ -78,11 +86,12 @@ pub enum Error {
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Error::Base58(ref e) => write!(f, "base58: {}", e),
Error::Bech32(ref e) => write!(f, "bech32: {}", e),
Error::Base58(_) => write!(f, "base58 address encoding error"),
Error::Bech32(_) => write!(f, "bech32 address encoding error"),
dr-orlovsky marked this conversation as resolved.
Show resolved Hide resolved
Error::EmptyBech32Payload => write!(f, "the bech32 payload was empty"),
Error::InvalidBech32Variant { expected, found } => write!(f, "invalid bech32 checksum variant found {:?} when {:?} was expected", found, expected),
Error::InvalidWitnessVersion(v) => write!(f, "invalid witness script version: {}", v),
Error::UnparsableWitnessVersion(ref e) => write!(f, "Incorrect format of a witness version byte: {}", e),
Error::UnparsableWitnessVersion(_) => write!(f, "incorrect format of a witness version byte"),
Error::MalformedWitnessVersion => f.write_str("bitcoin script opcode does not match any known witness version, the script is malformed"),
Error::InvalidWitnessProgramLength(l) => write!(f,
"the witness program must be between 2 and 40 bytes in length: length={}", l,
Expand Down Expand Up @@ -137,6 +146,8 @@ pub enum AddressType {
P2wpkh,
/// pay-to-witness-script-hash
P2wsh,
/// pay-to-taproot
P2tr,
}

impl fmt::Display for AddressType {
Expand All @@ -146,6 +157,7 @@ impl fmt::Display for AddressType {
AddressType::P2sh => "p2sh",
AddressType::P2wpkh => "p2wpkh",
AddressType::P2wsh => "p2wsh",
AddressType::P2tr => "p2tr",
})
}
}
Expand All @@ -158,6 +170,7 @@ impl FromStr for AddressType {
"p2sh" => Ok(AddressType::P2sh),
"p2wpkh" => Ok(AddressType::P2wpkh),
"p2wsh" => Ok(AddressType::P2wsh),
"p2tr" => Ok(AddressType::P2tr),
_ => Err(()),
}
}
Expand Down Expand Up @@ -314,6 +327,14 @@ impl WitnessVersion {
pub fn into_num(self) -> u8 {
self as u8
}

/// Determine the checksum variant. See BIP-0350 for specification.
pub fn bech32_variant(&self) -> bech32::Variant {
sgeisler marked this conversation as resolved.
Show resolved Hide resolved
match self {
WitnessVersion::V0 => bech32::Variant::Bech32,
_ => bech32::Variant::Bech32m,
dr-orlovsky marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

impl From<WitnessVersion> for ::bech32::u5 {
Expand Down Expand Up @@ -490,6 +511,17 @@ impl Address {
}
}

/// Create a pay to taproot address
pub fn p2tr(taptweaked_key: schnorrsig::PublicKey, network: Network) -> Address {
Address {
network: network,
payload: Payload::WitnessProgram {
version: WitnessVersion::V1,
program: taptweaked_key.serialize().to_vec()
}
}
}
sgeisler marked this conversation as resolved.
Show resolved Hide resolved

/// Get the address type of the address.
/// None if unknown, non-standard or related to the future witness version.
pub fn address_type(&self) -> Option<AddressType> {
Expand All @@ -507,6 +539,7 @@ impl Address {
32 => Some(AddressType::P2wsh),
_ => None,
},
WitnessVersion::V1 if prog.len() == 32 => Some(AddressType::P2tr),
dr-orlovsky marked this conversation as resolved.
Show resolved Hide resolved
_ => None,
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

match (version, prog.len()) would look a bit nicer but not a big deal.

But thinking about it isn't Address supposed to guarantee that lengths are always valid? This is checked at parsing and I don't seen an alternative unchecked constructor. If I'm right then these checks should be unnecessary. If not, then maybe it'd be better to have (private) fn check_program_len(self, len: usize) -> Result<(), Error> method on WitnessVersion and use it in both places to DRY.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about this myself. However, one day somebody may introduce new methods to create an address without checks and without this error checking we will get problems. So I think it is better to have an unreachable error checking code rather than assume everything should be fine due to the code in other external methods and unwrap/expect.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, maybe a more relevant question is if we should commit into address having this invariant.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is outside of the scope of this PR and may be controversial on how to achieve this

}
Expand Down Expand Up @@ -617,15 +650,14 @@ impl fmt::Display for Address {
Network::Testnet | Network::Signet => "tb",
Network::Regtest => "bcrt",
};
let bech_ver = if version.into_num() > 0 { bech32::Variant::Bech32m } else { bech32::Variant::Bech32 };
let mut upper_writer;
let writer = if fmt.alternate() {
upper_writer = UpperWriter(fmt);
&mut upper_writer as &mut dyn fmt::Write
} else {
fmt as &mut dyn fmt::Write
};
let mut bech32_writer = bech32::Bech32Writer::new(hrp, bech_ver, writer)?;
let mut bech32_writer = bech32::Bech32Writer::new(hrp, version.bech32_variant(), writer)?;
bech32::WriteBase32::write_u5(&mut bech32_writer, version.into())?;
bech32::ToBase32::write_base32(&prog, &mut bech32_writer)
}
Expand Down Expand Up @@ -688,10 +720,10 @@ impl FromStr for Address {
return Err(Error::InvalidSegwitV0ProgramLength(program.len()));
}

// Bech32 encoding check
if (version.into_num() > 0 && variant != bech32::Variant::Bech32m) ||
(version.into_num() == 0 && variant != bech32::Variant::Bech32) {
return Err(Error::InvalidWitnessVersion(version.into_num()))
// Encoding check
let expected = version.bech32_variant();
if expected != variant {
return Err(Error::InvalidBech32Variant { expected, found: variant });
}

return Ok(Address {
Expand Down
1 change: 1 addition & 0 deletions src/util/misc.rs
Expand Up @@ -168,6 +168,7 @@ mod message_signing {
Some(AddressType::P2sh) => false,
Some(AddressType::P2wpkh) => false,
Some(AddressType::P2wsh) => false,
Some(AddressType::P2tr) => false,
None => false,
})
}
Expand Down