Make Payload private and inline functionality

Currently we have functions on `Address` that call through to a public
`Payload` type. The `Payload` type is an implementation detail and
should never have been public. In preparation for modifying the
`AddressInner` and removing `Payload` altogether lets move all the
functionality from `Payload` into `Address` - this is basically just
code moves so it is feasible to review with some confidence.

This is an API breaking change because it makes `Payload` private and
also removes from the pubic `Address` API functions that accept and
return `Payload`. Apart from that the changes can be seen as
refactoring.
This commit is contained in:
Tobin C. Harding 2023-08-08 15:48:14 +10:00
parent b12bf07232
commit 7f75447c1d
No known key found for this signature in database
GPG Key ID: 40BF9E4C269D6607
1 changed files with 94 additions and 141 deletions

View File

@ -97,8 +97,7 @@ impl FromStr for AddressType {
/// The method used to produce an address. /// The method used to produce an address.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive] enum Payload {
pub enum Payload {
/// P2PKH address. /// P2PKH address.
PubkeyHash(PubkeyHash), PubkeyHash(PubkeyHash),
/// P2SH address. /// P2SH address.
@ -107,128 +106,6 @@ pub enum Payload {
WitnessProgram(WitnessProgram), WitnessProgram(WitnessProgram),
} }
impl Payload {
/// Constructs a [Payload] from an output script (`scriptPubkey`).
pub fn from_script(script: &Script) -> Result<Payload, Error> {
Ok(if script.is_p2pkh() {
let bytes = script.as_bytes()[3..23].try_into().expect("statically 20B long");
Payload::PubkeyHash(PubkeyHash::from_byte_array(bytes))
} else if script.is_p2sh() {
let bytes = script.as_bytes()[2..22].try_into().expect("statically 20B long");
Payload::ScriptHash(ScriptHash::from_byte_array(bytes))
} else if script.is_witness_program() {
let opcode = script.first_opcode().expect("witness_version guarantees len() > 4");
let witness_program = script.as_bytes()[2..].to_vec();
let witness_program =
WitnessProgram::new(WitnessVersion::try_from(opcode)?, witness_program)?;
Payload::WitnessProgram(witness_program)
} else {
return Err(Error::UnrecognizedScript);
})
}
/// Generates a script pubkey spending to this [Payload].
pub fn script_pubkey(&self) -> ScriptBuf {
match *self {
Payload::PubkeyHash(ref hash) => ScriptBuf::new_p2pkh(hash),
Payload::ScriptHash(ref hash) => ScriptBuf::new_p2sh(hash),
Payload::WitnessProgram(ref prog) => ScriptBuf::new_witness_program(prog),
}
}
/// Returns true if the address creates a particular script
/// This function doesn't make any allocations.
pub fn matches_script_pubkey(&self, script: &Script) -> bool {
match *self {
Payload::PubkeyHash(ref hash) if script.is_p2pkh() =>
&script.as_bytes()[3..23] == <PubkeyHash as AsRef<[u8; 20]>>::as_ref(hash),
Payload::ScriptHash(ref hash) if script.is_p2sh() =>
&script.as_bytes()[2..22] == <ScriptHash as AsRef<[u8; 20]>>::as_ref(hash),
Payload::WitnessProgram(ref prog) if script.is_witness_program() =>
&script.as_bytes()[2..] == prog.program().as_bytes(),
Payload::PubkeyHash(_) | Payload::ScriptHash(_) | Payload::WitnessProgram(_) => false,
}
}
/// Creates a pay to (compressed) public key hash payload from a public key
#[inline]
pub fn p2pkh(pk: &PublicKey) -> Payload { Payload::PubkeyHash(pk.pubkey_hash()) }
/// Creates a pay to script hash P2SH payload from a script
#[inline]
pub fn p2sh(script: &Script) -> Result<Payload, Error> {
if script.len() > MAX_SCRIPT_ELEMENT_SIZE {
return Err(Error::ExcessiveScriptSize);
}
Ok(Payload::ScriptHash(script.script_hash()))
}
/// Create a witness pay to public key payload from a public key
pub fn p2wpkh(pk: &PublicKey) -> Result<Payload, Error> {
let prog = WitnessProgram::new(
WitnessVersion::V0,
pk.wpubkey_hash().ok_or(Error::UncompressedPubkey)?,
)?;
Ok(Payload::WitnessProgram(prog))
}
/// Create a pay to script payload that embeds a witness pay to public key
pub fn p2shwpkh(pk: &PublicKey) -> Result<Payload, Error> {
let builder = script::Builder::new()
.push_int(0)
.push_slice(pk.wpubkey_hash().ok_or(Error::UncompressedPubkey)?);
Ok(Payload::ScriptHash(builder.into_script().script_hash()))
}
/// Create a witness pay to script hash payload.
pub fn p2wsh(script: &Script) -> Payload {
let prog = WitnessProgram::new(WitnessVersion::V0, script.wscript_hash())
.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
pub fn p2shwsh(script: &Script) -> Payload {
let ws = script::Builder::new().push_int(0).push_slice(script.wscript_hash()).into_script();
Payload::ScriptHash(ws.script_hash())
}
/// Create a pay to taproot payload from untweaked key
pub fn p2tr<C: Verification>(
secp: &Secp256k1<C>,
internal_key: UntweakedPublicKey,
merkle_root: Option<TapNodeHash>,
) -> Payload {
let (output_key, _parity) = internal_key.tap_tweak(secp, merkle_root);
let prog = WitnessProgram::new(WitnessVersion::V1, output_key.to_inner().serialize())
.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 {
let prog = WitnessProgram::new(WitnessVersion::V1, output_key.to_inner().serialize())
.expect("taproot output key has len 32 <= 40");
Payload::WitnessProgram(prog)
}
/// 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(prog) => prog.program().as_bytes(),
}
}
}
/// A utility struct to encode an address payload with the given parameters. /// A utility struct to encode an address payload with the given parameters.
/// This is a low-level utility struct. Consider using `Address` instead. /// This is a low-level utility struct. Consider using `Address` instead.
struct AddressEncoding<'a> { struct AddressEncoding<'a> {
@ -437,7 +314,7 @@ impl<N: NetworkValidation> serde::Serialize for Address<N> {
/// `Address<NetworkUnchecked>`. /// `Address<NetworkUnchecked>`.
impl<V: NetworkValidation> Address<V> { impl<V: NetworkValidation> Address<V> {
/// Returns a reference to the payload of this address. /// Returns a reference to the payload of this address.
pub fn payload(&self) -> &Payload { &self.0.payload } fn payload(&self) -> &Payload { &self.0.payload }
/// Returns a reference to the network of this address. /// Returns a reference to the network of this address.
pub fn network(&self) -> &Network { &self.0.network } pub fn network(&self) -> &Network { &self.0.network }
@ -449,7 +326,7 @@ impl<V: NetworkValidation> Address<V> {
} }
/// Extracts and returns the network and payload components of the `Address`. /// Extracts and returns the network and payload components of the `Address`.
pub fn into_parts(self) -> (Network, Payload) { fn into_parts(self) -> (Network, Payload) {
let AddressInner { payload, network } = self.0; let AddressInner { payload, network } = self.0;
(network, payload) (network, payload)
} }
@ -507,8 +384,7 @@ impl<V: NetworkValidation> Address<V> {
/// Create new address from given components, infering the network validation /// Create new address from given components, infering the network validation
/// marker type of the address. /// marker type of the address.
#[inline] fn new(network: Network, payload: Payload) -> Self {
pub fn new(network: Network, payload: Payload) -> Self {
Self(AddressInner { network, payload }, PhantomData) Self(AddressInner { network, payload }, PhantomData)
} }
} }
@ -520,7 +396,8 @@ impl Address {
/// This is the preferred non-witness type address. /// This is the preferred non-witness type address.
#[inline] #[inline]
pub fn p2pkh(pk: &PublicKey, network: Network) -> Address { pub fn p2pkh(pk: &PublicKey, network: Network) -> Address {
Address::new(network, Payload::p2pkh(pk)) let payload = Payload::PubkeyHash(pk.pubkey_hash());
Address(AddressInner { network, payload }, PhantomData)
} }
/// Creates a pay to script hash P2SH address from a script. /// Creates a pay to script hash P2SH address from a script.
@ -529,7 +406,11 @@ impl Address {
/// these days. /// these days.
#[inline] #[inline]
pub fn p2sh(script: &Script, network: Network) -> Result<Address, Error> { pub fn p2sh(script: &Script, network: Network) -> Result<Address, Error> {
Ok(Address::new(network, Payload::p2sh(script)?)) if script.len() > MAX_SCRIPT_ELEMENT_SIZE {
return Err(Error::ExcessiveScriptSize);
}
let payload = Payload::ScriptHash(script.script_hash());
Ok(Address(AddressInner { network, payload }, PhantomData))
} }
/// Creates a witness pay to public key address from a public key. /// Creates a witness pay to public key address from a public key.
@ -539,7 +420,12 @@ impl Address {
/// # Errors /// # Errors
/// Will only return an error if an uncompressed public key is provided. /// Will only return an error if an uncompressed public key is provided.
pub fn p2wpkh(pk: &PublicKey, network: Network) -> Result<Address, Error> { pub fn p2wpkh(pk: &PublicKey, network: Network) -> Result<Address, Error> {
Ok(Address::new(network, Payload::p2wpkh(pk)?)) let prog = WitnessProgram::new(
WitnessVersion::V0,
pk.wpubkey_hash().ok_or(Error::UncompressedPubkey)?,
)?;
let payload = Payload::WitnessProgram(prog);
Ok(Address(AddressInner { network, payload }, PhantomData))
} }
/// Creates a pay to script address that embeds a witness pay to public key. /// Creates a pay to script address that embeds a witness pay to public key.
@ -549,19 +435,29 @@ impl Address {
/// # Errors /// # Errors
/// Will only return an Error if an uncompressed public key is provided. /// Will only return an Error if an uncompressed public key is provided.
pub fn p2shwpkh(pk: &PublicKey, network: Network) -> Result<Address, Error> { pub fn p2shwpkh(pk: &PublicKey, network: Network) -> Result<Address, Error> {
Ok(Address::new(network, Payload::p2shwpkh(pk)?)) let builder = script::Builder::new()
.push_int(0)
.push_slice(pk.wpubkey_hash().ok_or(Error::UncompressedPubkey)?);
let payload = Payload::ScriptHash(builder.into_script().script_hash());
Ok(Address(AddressInner { network, payload }, PhantomData))
} }
/// Creates a witness pay to script hash address. /// Creates a witness pay to script hash address.
pub fn p2wsh(script: &Script, network: Network) -> Address { pub fn p2wsh(script: &Script, network: Network) -> Address {
Address::new(network, Payload::p2wsh(script)) let prog = WitnessProgram::new(WitnessVersion::V0, script.wscript_hash())
.expect("wscript_hash has len 32 compatible with segwitv0");
let payload = Payload::WitnessProgram(prog);
Address(AddressInner { network, payload }, PhantomData)
} }
/// Creates a pay to script address that embeds a witness pay to script hash address. /// Creates a pay to script address that embeds a witness pay to script hash address.
/// ///
/// This is a segwit address type that looks familiar (as p2sh) to legacy clients. /// This is a segwit address type that looks familiar (as p2sh) to legacy clients.
pub fn p2shwsh(script: &Script, network: Network) -> Address { pub fn p2shwsh(script: &Script, network: Network) -> Address {
Address::new(network, Payload::p2shwsh(script)) let ws = script::Builder::new().push_int(0).push_slice(script.wscript_hash()).into_script();
let payload = Payload::ScriptHash(ws.script_hash());
Address(AddressInner { network, payload }, PhantomData)
} }
/// Creates a pay to taproot address from an untweaked key. /// Creates a pay to taproot address from an untweaked key.
@ -571,14 +467,30 @@ impl Address {
merkle_root: Option<TapNodeHash>, merkle_root: Option<TapNodeHash>,
network: Network, network: Network,
) -> Address { ) -> Address {
Address::new(network, Payload::p2tr(secp, internal_key, merkle_root)) let (output_key, _parity) = internal_key.tap_tweak(secp, merkle_root);
let prog = WitnessProgram::new(WitnessVersion::V1, output_key.to_inner().serialize())
.expect("taproot output key has len 32 <= 40");
let payload = Payload::WitnessProgram(prog);
Address(AddressInner { network, payload }, PhantomData)
} }
/// Creates a pay to taproot address from a pre-tweaked output key. /// Creates a pay to taproot address from a pre-tweaked output key.
/// ///
/// This method is not recommended for use, [`Address::p2tr()`] should be used where possible. /// This method is not recommended for use, [`Address::p2tr()`] should be used where possible.
pub fn p2tr_tweaked(output_key: TweakedPublicKey, network: Network) -> Address { pub fn p2tr_tweaked(output_key: TweakedPublicKey, network: Network) -> Address {
Address::new(network, Payload::p2tr_tweaked(output_key)) let prog = WitnessProgram::new(WitnessVersion::V1, output_key.to_inner().serialize())
.expect("taproot output key has len 32 <= 40");
let payload = Payload::WitnessProgram(prog);
Address(AddressInner { network, payload }, PhantomData)
}
/// Creates an address from an arbitrary witness program.
///
/// This only exists to support future witness versions. If you are doing normal mainnet things
/// then you likely do not need this constructor.
pub fn from_witness_program(program: WitnessProgram, network: Network) -> Address {
let inner = AddressInner { payload: Payload::WitnessProgram(program), network };
Address(inner, PhantomData)
} }
/// Gets the address type of the address. /// Gets the address type of the address.
@ -605,11 +517,34 @@ impl Address {
/// Constructs an [`Address`] from an output script (`scriptPubkey`). /// Constructs an [`Address`] from an output script (`scriptPubkey`).
pub fn from_script(script: &Script, network: Network) -> Result<Address, Error> { pub fn from_script(script: &Script, network: Network) -> Result<Address, Error> {
Ok(Address::new(network, Payload::from_script(script)?)) let payload = if script.is_p2pkh() {
let bytes = script.as_bytes()[3..23].try_into().expect("statically 20B long");
Payload::PubkeyHash(PubkeyHash::from_byte_array(bytes))
} else if script.is_p2sh() {
let bytes = script.as_bytes()[2..22].try_into().expect("statically 20B long");
Payload::ScriptHash(ScriptHash::from_byte_array(bytes))
} else if script.is_witness_program() {
let opcode = script.first_opcode().expect("witness_version guarantees len() > 4");
let witness_program = script.as_bytes()[2..].to_vec();
let witness_program =
WitnessProgram::new(WitnessVersion::try_from(opcode)?, witness_program)?;
Payload::WitnessProgram(witness_program)
} else {
return Err(Error::UnrecognizedScript);
};
Ok(Address(AddressInner { network, payload }, PhantomData))
} }
/// Generates a script pubkey spending to this address. /// Generates a script pubkey spending to this address.
pub fn script_pubkey(&self) -> ScriptBuf { self.payload().script_pubkey() } pub fn script_pubkey(&self) -> ScriptBuf {
match self.payload() {
Payload::PubkeyHash(ref hash) => ScriptBuf::new_p2pkh(hash),
Payload::ScriptHash(ref hash) => ScriptBuf::new_p2sh(hash),
Payload::WitnessProgram(ref prog) => ScriptBuf::new_witness_program(prog),
}
}
/// Creates a URI string *bitcoin:address* optimized to be encoded in QR codes. /// Creates a URI string *bitcoin:address* optimized to be encoded in QR codes.
/// ///
@ -647,7 +582,7 @@ impl Address {
/// given key. For taproot addresses, the supplied key is assumed to be tweaked /// given key. For taproot addresses, the supplied key is assumed to be tweaked
pub fn is_related_to_pubkey(&self, pubkey: &PublicKey) -> bool { pub fn is_related_to_pubkey(&self, pubkey: &PublicKey) -> bool {
let pubkey_hash = pubkey.pubkey_hash(); let pubkey_hash = pubkey.pubkey_hash();
let payload = self.payload().inner_prog_as_bytes(); let payload = self.inner_prog_as_bytes();
let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner); let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner);
(*pubkey_hash.as_byte_array() == *payload) (*pubkey_hash.as_byte_array() == *payload)
@ -660,14 +595,32 @@ impl Address {
/// This will only work for Taproot addresses. The Public Key is /// This will only work for Taproot addresses. The Public Key is
/// assumed to have already been tweaked. /// assumed to have already been tweaked.
pub fn is_related_to_xonly_pubkey(&self, xonly_pubkey: &XOnlyPublicKey) -> bool { pub fn is_related_to_xonly_pubkey(&self, xonly_pubkey: &XOnlyPublicKey) -> bool {
let payload = self.payload().inner_prog_as_bytes(); let payload = self.inner_prog_as_bytes();
payload == xonly_pubkey.serialize() payload == xonly_pubkey.serialize()
} }
/// Returns true if the address creates a particular script /// Returns true if the address creates a particular script
/// This function doesn't make any allocations. /// This function doesn't make any allocations.
pub fn matches_script_pubkey(&self, script_pubkey: &Script) -> bool { pub fn matches_script_pubkey(&self, script_pubkey: &Script) -> bool {
self.payload().matches_script_pubkey(script_pubkey) match &self.payload() {
Payload::PubkeyHash(ref hash) if script_pubkey.is_p2pkh() =>
&script_pubkey.as_bytes()[3..23] == <PubkeyHash as AsRef<[u8; 20]>>::as_ref(hash),
Payload::ScriptHash(ref hash) if script_pubkey.is_p2sh() =>
&script_pubkey.as_bytes()[2..22] == <ScriptHash as AsRef<[u8; 20]>>::as_ref(hash),
Payload::WitnessProgram(ref prog) if script_pubkey.is_witness_program() =>
&script_pubkey.as_bytes()[2..] == prog.program().as_bytes(),
Payload::PubkeyHash(_) | Payload::ScriptHash(_) | Payload::WitnessProgram(_) => false,
}
}
/// 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() {
Payload::ScriptHash(hash) => hash.as_ref(),
Payload::PubkeyHash(hash) => hash.as_ref(),
Payload::WitnessProgram(prog) => prog.program().as_bytes(),
}
} }
} }