Merge rust-bitcoin/rust-bitcoin#1564: Address validity invariant cleanups

44d3ec487d Rename Payload::as_bytes to inner_prog_as_bytes (sanket1729)
a446df583c Make Payload non-exhaustive (sanket1729)
6ebc9de252 Introduce WitnessProgram struct and cleanup Address validity invariants (sanket1729)
41652caf05 Introduce is_spend_standard method (sanket1729)

Pull request description:

  Fixes #1561.

  Highlights:

  - Segwitv0 programs with lengths apart from 20 or 32 are invalid `Address` struct. Such Addresses are useless and we should not parse/create them.
  - Renamed `is_standard` to `is_spend_standard`.

ACKs for top commit:
  apoelstra:
    ACK 44d3ec487d
  tcharding:
    ACK 44d3ec487d

Tree-SHA512: 1ee36f7ea25c65619ddf7d643d025690096876843dbe6fbdf877ce23e88049d79b0bbd78cee6cf4b415bca028b3634bb70c9f52d1098bd90558e6ba7f8731332
This commit is contained in:
Andrew Poelstra 2023-01-24 13:14:53 +00:00
commit d8d34116ad
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
3 changed files with 132 additions and 68 deletions

View File

@ -386,18 +386,48 @@ impl From<WitnessVersion> for opcodes::All {
/// The method used to produce an address.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Payload {
/// P2PKH address.
PubkeyHash(PubkeyHash),
/// P2SH address.
ScriptHash(ScriptHash),
/// Segwit address.
WitnessProgram {
WitnessProgram(WitnessProgram),
}
/// Witness program as defined in BIP141.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WitnessProgram {
/// The witness program version.
version: WitnessVersion,
/// The witness program.
/// The witness program. (Between 2 and 40 bytes)
program: Vec<u8>,
},
}
impl WitnessProgram {
/// Creates a new witness program.
pub fn new(version: WitnessVersion, program: Vec<u8>) -> Result<Self, Error> {
if program.len() < 2 || program.len() > 40 {
return Err(Error::InvalidWitnessProgramLength(program.len()));
}
// Specific segwit v0 check. These addresses can never spend funds sent to them.
if version == WitnessVersion::V0 && (program.len() != 20 && program.len() != 32) {
return Err(Error::InvalidSegwitV0ProgramLength(program.len()));
}
Ok(WitnessProgram { version, program })
}
/// Returns the witness program version.
pub fn version(&self) -> WitnessVersion {
self.version
}
/// Returns the witness program.
pub fn program(&self) -> &[u8] {
&self.program
}
}
impl Payload {
@ -412,18 +442,13 @@ impl Payload {
hash_inner.copy_from_slice(&script.as_bytes()[2..22]);
Payload::ScriptHash(ScriptHash::from_inner(hash_inner))
} else if script.is_witness_program() {
if script.witness_version() == Some(WitnessVersion::V0)
&& !(script.is_v0_p2wpkh() || script.is_v0_p2wsh())
{
return Err(Error::InvalidSegwitV0ProgramLength(script.len() - 2));
}
let opcode = script.first_opcode().expect("witness_version guarantees len() > 4");
Payload::WitnessProgram {
version: WitnessVersion::try_from(opcode)?,
program: script.as_bytes()[2..].to_vec(),
}
let witness_program = WitnessProgram::new(
WitnessVersion::try_from(opcode)?,
script.as_bytes()[2..].to_vec(),
)?;
Payload::WitnessProgram(witness_program)
} else {
return Err(Error::UnrecognizedScript);
})
@ -434,8 +459,8 @@ impl Payload {
match *self {
Payload::PubkeyHash(ref hash) => script::ScriptBuf::new_p2pkh(hash),
Payload::ScriptHash(ref hash) => script::ScriptBuf::new_p2sh(hash),
Payload::WitnessProgram { version, program: ref prog } =>
script::ScriptBuf::new_witness_program(version, prog)
Payload::WitnessProgram(ref prog) =>
script::ScriptBuf::new_witness_program(prog)
}
}
@ -454,10 +479,11 @@ impl Payload {
/// Create a witness pay to public key payload from a public key
pub fn p2wpkh(pk: &PublicKey) -> Result<Payload, Error> {
Ok(Payload::WitnessProgram {
version: WitnessVersion::V0,
program: pk.wpubkey_hash().ok_or(Error::UncompressedPubkey)?.as_ref().to_vec(),
})
let prog = WitnessProgram::new(
WitnessVersion::V0,
pk.wpubkey_hash().ok_or(Error::UncompressedPubkey)?.as_ref().to_vec()
)?;
Ok(Payload::WitnessProgram(prog))
}
/// Create a pay to script payload that embeds a witness pay to public key
@ -471,10 +497,11 @@ impl Payload {
/// Create a witness pay to script hash payload.
pub fn p2wsh(script: &script::Script) -> Payload {
Payload::WitnessProgram {
version: WitnessVersion::V0,
program: script.wscript_hash().as_ref().to_vec(),
}
let prog = WitnessProgram::new(
WitnessVersion::V0,
script.wscript_hash().as_ref().to_vec()
).expect("wscript_hash has len 32 compatible with segwitv0");
Payload::WitnessProgram(prog)
}
/// Create a pay to script payload that embeds a witness pay to script hash address
@ -492,28 +519,31 @@ impl Payload {
merkle_root: Option<TapNodeHash>,
) -> Payload {
let (output_key, _parity) = internal_key.tap_tweak(secp, merkle_root);
Payload::WitnessProgram {
version: WitnessVersion::V1,
program: output_key.to_inner().serialize().to_vec(),
}
let prog = WitnessProgram::new(
WitnessVersion::V1,
output_key.to_inner().serialize().to_vec()
).expect("taproot output key has len 32 <= 40");
Payload::WitnessProgram(prog)
}
/// Create a pay to taproot payload from a pre-tweaked output key.
///
/// This method is not recommended for use and [Payload::p2tr()] should be used where possible.
pub fn p2tr_tweaked(output_key: TweakedPublicKey) -> Payload {
Payload::WitnessProgram {
version: WitnessVersion::V1,
program: output_key.to_inner().serialize().to_vec(),
}
let prog = WitnessProgram::new(
WitnessVersion::V1,
output_key.to_inner().serialize().to_vec()
).expect("taproot output key has len 32 <= 40");
Payload::WitnessProgram(prog)
}
/// Returns a byte slice of the payload
pub fn as_bytes(&self) -> &[u8] {
/// Returns a byte slice of the inner program of the payload. If the payload
/// is a script hash or pubkey hash, a reference to the hash is returned.
fn inner_prog_as_bytes(&self) -> &[u8] {
match self {
Payload::ScriptHash(hash) => hash.as_ref(),
Payload::PubkeyHash(hash) => hash.as_ref(),
Payload::WitnessProgram { program, .. } => program,
Payload::WitnessProgram(prog) => prog.program(),
}
}
}
@ -546,7 +576,8 @@ impl<'a> fmt::Display for AddressEncoding<'a> {
prefixed[1..].copy_from_slice(&hash[..]);
base58::encode_check_to_fmt(fmt, &prefixed[..])
}
Payload::WitnessProgram { version, program: prog } => {
Payload::WitnessProgram(witness_prog) => {
let (version, prog) = (witness_prog.version(), witness_prog.program());
let mut upper_writer;
let writer = if fmt.alternate() {
upper_writer = UpperWriter(fmt);
@ -556,7 +587,7 @@ impl<'a> fmt::Display for AddressEncoding<'a> {
};
let mut bech32_writer =
bech32::Bech32Writer::new(self.bech32_hrp, version.bech32_variant(), writer)?;
bech32::WriteBase32::write_u5(&mut bech32_writer, (*version).into())?;
bech32::WriteBase32::write_u5(&mut bech32_writer, version.into())?;
bech32::ToBase32::write_base32(&prog, &mut bech32_writer)
}
}
@ -728,15 +759,15 @@ impl<V: NetworkValidation> Address<V> {
match self.payload {
Payload::PubkeyHash(_) => Some(AddressType::P2pkh),
Payload::ScriptHash(_) => Some(AddressType::P2sh),
Payload::WitnessProgram { version, program: ref prog } => {
Payload::WitnessProgram(ref prog) => {
// BIP-141 p2wpkh or p2wsh addresses.
match version {
WitnessVersion::V0 => match prog.len() {
match prog.version() {
WitnessVersion::V0 => match prog.program().len() {
20 => Some(AddressType::P2wpkh),
32 => Some(AddressType::P2wsh),
_ => None,
_ => unreachable!("Address creation invariant violation: invalid program length")
},
WitnessVersion::V1 if prog.len() == 32 => Some(AddressType::P2tr),
WitnessVersion::V1 if prog.program().len() == 32 => Some(AddressType::P2tr),
_ => None,
}
}
@ -851,10 +882,26 @@ impl Address {
self.address_type_internal()
}
/// Checks whether or not the address is following Bitcoin standardness rules when
/// *spending* from this address. *NOT* to be called by senders.
///
/// <details>
/// <summary>Spending Standardness</summary>
///
/// For forward compatibility, the senders must send to any [`Address`]. Receivers
/// can use this method to check whether or not they can spend from this address.
///
/// SegWit addresses with unassigned witness versions or non-standard program sizes are
/// considered non-standard.
/// </details>
///
pub fn is_spend_standard(&self) -> bool { self.address_type().is_some() }
/// Checks whether or not the address is following Bitcoin standardness rules.
///
/// SegWit addresses with unassigned witness versions or non-standard program sizes are
/// considered non-standard.
#[deprecated(since = "0.30.0", note = "Use Address::is_spend_standard instead")]
pub fn is_standard(&self) -> bool { self.address_type().is_some() }
/// Constructs an [`Address`] from an output script (`scriptPubkey`).
@ -887,7 +934,7 @@ impl Address {
/// given key. For taproot addresses, the supplied key is assumed to be tweaked
pub fn is_related_to_pubkey(&self, pubkey: &PublicKey) -> bool {
let pubkey_hash = pubkey.pubkey_hash();
let payload = self.payload.as_bytes();
let payload = self.payload.inner_prog_as_bytes();
let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner);
(*pubkey_hash.as_ref() == *payload)
@ -900,7 +947,7 @@ impl Address {
/// This will only work for Taproot addresses. The Public Key is
/// assumed to have already been tweaked.
pub fn is_related_to_xonly_pubkey(&self, xonly_pubkey: &XOnlyPublicKey) -> bool {
let payload = self.payload.as_bytes();
let payload = self.payload.inner_prog_as_bytes();
payload == xonly_pubkey.serialize()
}
}
@ -1034,14 +1081,7 @@ impl FromStr for Address<NetworkUnchecked> {
(WitnessVersion::try_from(v[0])?, bech32::FromBase32::from_base32(p5)?)
};
if program.len() < 2 || program.len() > 40 {
return Err(Error::InvalidWitnessProgramLength(program.len()));
}
// Specific segwit v0 check.
if version == WitnessVersion::V0 && (program.len() != 20 && program.len() != 32) {
return Err(Error::InvalidSegwitV0ProgramLength(program.len()));
}
let witness_program = WitnessProgram::new(version, program)?;
// Encoding check
let expected = version.bech32_variant();
@ -1049,7 +1089,7 @@ impl FromStr for Address<NetworkUnchecked> {
return Err(Error::InvalidBech32Variant { expected, found: variant });
}
return Ok(Address::new(network, Payload::WitnessProgram { version, program }));
return Ok(Address::new(network, Payload::WitnessProgram(witness_program)));
}
// Base58
@ -1231,7 +1271,8 @@ mod tests {
let program = hex!(
"654f6ea368e0acdfd92976b7c2103a1b26313f430654f6ea368e0acdfd92976b7c2103a1b26313f4"
);
let addr = Address::new(Bitcoin, Payload::WitnessProgram { version: WitnessVersion::V13, program });
let witness_prog = WitnessProgram::new(WitnessVersion::V13, program).unwrap();
let addr = Address::new(Bitcoin, Payload::WitnessProgram(witness_prog));
roundtrips(&addr);
}
@ -1461,10 +1502,10 @@ mod tests {
Payload::ScriptHash(ScriptHash::all_zeros()),
];
let segwit_payload = (0..=16)
.map(|version| Payload::WitnessProgram {
version: WitnessVersion::try_from(version).unwrap(),
program: vec![],
})
.map(|version| Payload::WitnessProgram(WitnessProgram::new(
WitnessVersion::try_from(version).unwrap(),
vec![0xab; 32], // Choose 32 to make test case valid for all witness versions(including v0)
).unwrap()))
.collect::<Vec<_>>();
const LEGACY_EQUIVALENCE_CLASSES: &[&[Network]] =

View File

@ -74,7 +74,7 @@ use crate::policy::DUST_RELAY_TX_FEE;
use crate::OutPoint;
use crate::key::PublicKey;
use crate::address::WitnessVersion;
use crate::address::{WitnessVersion, WitnessProgram};
use crate::taproot::{LeafVersion, TapNodeHash, TapLeafHash};
use secp256k1::{Secp256k1, Verification, XOnlyPublicKey};
use crate::schnorr::{TapTweak, TweakedPublicKey, UntweakedPublicKey};
@ -1122,28 +1122,47 @@ impl ScriptBuf {
/// Generates P2WPKH-type of scriptPubkey.
pub fn new_v0_p2wpkh(pubkey_hash: &WPubkeyHash) -> Self {
ScriptBuf::new_witness_program(WitnessVersion::V0, &pubkey_hash[..])
// pubkey hash is 20 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv0)
ScriptBuf::new_witness_program_unchecked(WitnessVersion::V0, &pubkey_hash[..])
}
/// Generates P2WSH-type of scriptPubkey with a given hash of the redeem script.
pub fn new_v0_p2wsh(script_hash: &WScriptHash) -> Self {
ScriptBuf::new_witness_program(WitnessVersion::V0, &script_hash[..])
// script hash is 32 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv0)
ScriptBuf::new_witness_program_unchecked(WitnessVersion::V0, &script_hash[..])
}
/// Generates P2TR for script spending path using an internal public key and some optional
/// script tree merkle root.
pub fn new_v1_p2tr<C: Verification>(secp: &Secp256k1<C>, internal_key: UntweakedPublicKey, merkle_root: Option<TapNodeHash>) -> Self {
let (output_key, _) = internal_key.tap_tweak(secp, merkle_root);
ScriptBuf::new_witness_program(WitnessVersion::V1, &output_key.serialize())
// output key is 32 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv1)
ScriptBuf::new_witness_program_unchecked(WitnessVersion::V1, &output_key.serialize())
}
/// Generates P2TR for key spending path for a known [`TweakedPublicKey`].
pub fn new_v1_p2tr_tweaked(output_key: TweakedPublicKey) -> Self {
ScriptBuf::new_witness_program(WitnessVersion::V1, &output_key.serialize())
// output key is 32 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv1)
ScriptBuf::new_witness_program_unchecked(WitnessVersion::V1, &output_key.serialize())
}
/// Generates P2WSH-type of scriptPubkey with a given hash of the redeem script.
pub fn new_witness_program(version: WitnessVersion, program: &[u8]) -> Self {
/// Generates P2WSH-type of scriptPubkey with a given [`WitnessProgram`].
pub fn new_witness_program(witness_program: &WitnessProgram) -> Self {
Builder::new()
.push_opcode(witness_program.version().into())
.push_slice(witness_program.program())
.into_script()
}
/// Generates P2WSH-type of scriptPubkey with a given [`WitnessVersion`] and the program bytes.
/// Does not do any checks on version or program length.
///
/// Convenience method used by `new_v0_p2wpkh`, `new_v0_p2wsh`, `new_v1_p2tr`, and
/// `new_v1_p2tr_tweaked`.
fn new_witness_program_unchecked(version: WitnessVersion, program: &[u8]) -> Self {
debug_assert!(program.len() >= 2 && program.len() <= 40);
// In segwit v0, the program must be 20 or 32 bytes long.
debug_assert!(version != WitnessVersion::V0 || program.len() == 20 || program.len() == 32);
Builder::new()
.push_opcode(version.into())
.push_slice(program)

View File

@ -1742,6 +1742,7 @@ mod tests {
#[cfg(feature = "rand-std")]
fn sign_psbt() {
use crate::WPubkeyHash;
use crate::address::WitnessProgram;
use crate::bip32::{Fingerprint, DerivationPath};
let unsigned_tx = Transaction {
@ -1771,9 +1772,12 @@ mod tests {
psbt.inputs[0].bip32_derivation = map;
// Second input is unspendable by us e.g., from another wallet that supports future upgrades.
let unknown_prog = WitnessProgram::new(
crate::address::WitnessVersion::V4, vec![0xaa; 34]
).unwrap();
let txout_unknown_future = TxOut{
value: 10,
script_pubkey: ScriptBuf::new_witness_program(crate::address::WitnessVersion::V4, &[0xaa; 34]),
script_pubkey: ScriptBuf::new_witness_program(&unknown_prog),
};
psbt.inputs[1].witness_utxo = Some(txout_unknown_future);