Move transparent transaction builder to an independent module.

This commit is contained in:
Kris Nuttycombe 2021-05-11 14:17:54 -06:00
parent 8267d06846
commit ba6fc053b5
3 changed files with 196 additions and 150 deletions

View File

@ -28,7 +28,8 @@ use crate::{
transaction::{
components::{
amount::{Amount, DEFAULT_FEE},
OutputDescription, SpendDescription, TxIn, TxOut,
transparent::builder::{self as transparent, TransparentBuilder},
OutputDescription, SpendDescription,
},
signature_hash_data, SignableInput, Transaction, TransactionData, TxVersion, SIGHASH_ALL,
},
@ -36,7 +37,7 @@ use crate::{
};
#[cfg(feature = "transparent-inputs")]
use crate::{legacy::Script, transaction::components::OutPoint};
use crate::transaction::components::{OutPoint, TxOut};
#[cfg(feature = "zfuture")]
use crate::{
@ -63,6 +64,7 @@ pub enum Error {
NoChangeAddress,
SpendProof,
TzeWitnessModeMismatch(u32, u32),
TransparentBuildError(transparent::Error),
}
impl fmt::Display for Error {
@ -81,6 +83,7 @@ impl fmt::Display for Error {
Error::SpendProof => write!(f, "Failed to create Sapling spend proof"),
Error::TzeWitnessModeMismatch(expected, actual) =>
write!(f, "TZE witness builder returned a mode that did not match the mode with which the input was initially constructed: expected = {:?}, actual = {:?}", expected, actual),
Error::TransparentBuildError(err) => err.fmt(f),
}
}
}
@ -198,148 +201,6 @@ impl<P: consensus::Parameters> SaplingOutput<P> {
}
}
#[cfg(feature = "transparent-inputs")]
struct TransparentInputInfo {
sk: secp256k1::SecretKey,
pubkey: [u8; secp256k1::constants::PUBLIC_KEY_SIZE],
utxo: OutPoint,
coin: TxOut,
}
struct TransparentBuilder {
#[cfg(feature = "transparent-inputs")]
secp: secp256k1::Secp256k1<secp256k1::SignOnly>,
#[cfg(feature = "transparent-inputs")]
inputs: Vec<TransparentInputInfo>,
vout: Vec<TxOut>,
}
impl TransparentBuilder {
fn new() -> Self {
TransparentBuilder {
#[cfg(feature = "transparent-inputs")]
secp: secp256k1::Secp256k1::gen_new(),
#[cfg(feature = "transparent-inputs")]
inputs: vec![],
vout: vec![],
}
}
#[cfg(feature = "transparent-inputs")]
fn add_input(
&mut self,
sk: secp256k1::SecretKey,
utxo: OutPoint,
coin: TxOut,
) -> Result<(), Error> {
if coin.value.is_negative() {
return Err(Error::InvalidAmount);
}
// Ensure that the RIPEMD-160 digest of the public key associated with the
// provided secret key matches that of the address to which the provided
// output may be spent.
let pubkey = secp256k1::PublicKey::from_secret_key(&self.secp, &sk).serialize();
match coin.script_pubkey.address() {
Some(TransparentAddress::PublicKey(hash)) => {
use ripemd160::Ripemd160;
use sha2::{Digest, Sha256};
if hash[..] != Ripemd160::digest(&Sha256::digest(&pubkey))[..] {
return Err(Error::InvalidAddress);
}
}
_ => return Err(Error::InvalidAddress),
}
self.inputs.push(TransparentInputInfo {
sk,
pubkey,
utxo,
coin,
});
Ok(())
}
fn add_output(&mut self, to: &TransparentAddress, value: Amount) -> Result<(), Error> {
if value.is_negative() {
return Err(Error::InvalidAmount);
}
self.vout.push(TxOut {
value,
script_pubkey: to.script(),
});
Ok(())
}
fn value_balance(&self) -> Option<Amount> {
#[cfg(feature = "transparent-inputs")]
let input_sum = self
.inputs
.iter()
.map(|input| input.coin.value)
.sum::<Option<Amount>>()?;
#[cfg(not(feature = "transparent-inputs"))]
let input_sum = Amount::zero();
input_sum
- self
.vout
.iter()
.map(|vo| vo.value)
.sum::<Option<Amount>>()?
}
fn build(&self) -> (Vec<TxIn>, Vec<TxOut>) {
#[cfg(feature = "transparent-inputs")]
let vin = self
.inputs
.iter()
.map(|i| TxIn::new(i.utxo.clone()))
.collect();
#[cfg(not(feature = "transparent-inputs"))]
let vin = vec![];
(vin, self.vout.clone())
}
#[cfg(feature = "transparent-inputs")]
fn create_signatures(
self,
mtx: &TransactionData,
consensus_branch_id: consensus::BranchId,
) -> Vec<Script> {
self.inputs
.iter()
.enumerate()
.map(|(i, info)| {
let mut sighash = [0u8; 32];
sighash.copy_from_slice(&signature_hash_data(
mtx,
consensus_branch_id,
SIGHASH_ALL,
SignableInput::transparent(i, &info.coin.script_pubkey, info.coin.value),
));
let msg = secp256k1::Message::from_slice(sighash.as_ref()).expect("32 bytes");
let sig = self.secp.sign(&msg, &info.sk);
// Signature has to have "SIGHASH_ALL" appended to it
let mut sig_bytes: Vec<u8> = sig.serialize_der()[..].to_vec();
sig_bytes.extend(&[SIGHASH_ALL as u8]);
// P2PKH scriptSig
Script::default() << &sig_bytes[..] << &info.pubkey[..]
})
.collect()
}
}
#[cfg(feature = "zfuture")]
#[allow(clippy::type_complexity)]
struct TzeSigner<'a, BuildCtx> {
@ -875,7 +736,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
target_height,
expiry_height: target_height + DEFAULT_TX_EXPIRY_DELTA,
fee: DEFAULT_FEE,
transparent_builder: TransparentBuilder::new(),
transparent_builder: TransparentBuilder::empty(),
sapling_builder: SaplingBuilder::new(),
#[cfg(feature = "zfuture")]
tze_builder: TzeBuilder::new(),
@ -928,7 +789,9 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
utxo: OutPoint,
coin: TxOut,
) -> Result<(), Error> {
self.transparent_builder.add_input(sk, utxo, coin)
self.transparent_builder
.add_input(sk, utxo, coin)
.map_err(Error::TransparentBuildError)
}
/// Adds a transparent address to send funds to.
@ -937,7 +800,9 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
to: &TransparentAddress,
value: Amount,
) -> Result<(), Error> {
self.transparent_builder.add_output(to, value)
self.transparent_builder
.add_output(to, value)
.map_err(Error::TransparentBuildError)
}
/// Sets the Sapling address to which any change will be sent.
@ -1181,7 +1046,9 @@ mod tests {
merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::{prover::mock::MockTxProver, Node, Rseed},
transaction::{
components::{amount::Amount, amount::DEFAULT_FEE},
components::{
amount::Amount, amount::DEFAULT_FEE, transparent::builder as transparent,
},
TxVersion,
},
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
@ -1222,7 +1089,7 @@ mod tests {
target_height: sapling_activation_height,
expiry_height: sapling_activation_height + DEFAULT_TX_EXPIRY_DELTA,
fee: Amount::zero(),
transparent_builder: TransparentBuilder::new(),
transparent_builder: TransparentBuilder::empty(),
sapling_builder: SaplingBuilder::new(),
#[cfg(feature = "zfuture")]
tze_builder: TzeBuilder::new(),
@ -1292,7 +1159,9 @@ mod tests {
&TransparentAddress::PublicKey([0; 20]),
Amount::from_i64(-1).unwrap(),
),
Err(Error::InvalidAmount)
Err(Error::TransparentBuildError(
transparent::Error::InvalidAmount
))
);
}

View File

@ -8,6 +8,8 @@ use crate::legacy::Script;
use super::amount::Amount;
pub mod builder;
#[derive(Clone, Debug, PartialEq)]
pub struct OutPoint {
hash: [u8; 32],

View File

@ -0,0 +1,175 @@
//! Types and functions for building transparent transaction components.
use std::fmt;
use crate::{
legacy::TransparentAddress,
transaction::components::{amount::Amount, TxIn, TxOut},
};
#[cfg(feature = "transparent-inputs")]
use crate::{
consensus::{self},
legacy::Script,
transaction::{
components::OutPoint, sighash::signature_hash_data, SignableInput, TransactionData,
SIGHASH_ALL,
},
};
#[derive(Debug, PartialEq)]
pub enum Error {
InvalidAddress,
InvalidAmount,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::InvalidAddress => write!(f, "Invalid address"),
Error::InvalidAmount => write!(f, "Invalid amount"),
}
}
}
#[cfg(feature = "transparent-inputs")]
struct TransparentInputInfo {
sk: secp256k1::SecretKey,
pubkey: [u8; secp256k1::constants::PUBLIC_KEY_SIZE],
utxo: OutPoint,
coin: TxOut,
}
pub struct TransparentBuilder {
#[cfg(feature = "transparent-inputs")]
secp: secp256k1::Secp256k1<secp256k1::SignOnly>,
#[cfg(feature = "transparent-inputs")]
inputs: Vec<TransparentInputInfo>,
vout: Vec<TxOut>,
}
impl TransparentBuilder {
pub fn empty() -> Self {
TransparentBuilder {
#[cfg(feature = "transparent-inputs")]
secp: secp256k1::Secp256k1::gen_new(),
#[cfg(feature = "transparent-inputs")]
inputs: vec![],
vout: vec![],
}
}
#[cfg(feature = "transparent-inputs")]
pub fn add_input(
&mut self,
sk: secp256k1::SecretKey,
utxo: OutPoint,
coin: TxOut,
) -> Result<(), Error> {
if coin.value.is_negative() {
return Err(Error::InvalidAmount);
}
// Ensure that the RIPEMD-160 digest of the public key associated with the
// provided secret key matches that of the address to which the provided
// output may be spent.
let pubkey = secp256k1::PublicKey::from_secret_key(&self.secp, &sk).serialize();
match coin.script_pubkey.address() {
Some(TransparentAddress::PublicKey(hash)) => {
use ripemd160::Ripemd160;
use sha2::{Digest, Sha256};
if hash[..] != Ripemd160::digest(&Sha256::digest(&pubkey))[..] {
return Err(Error::InvalidAddress);
}
}
_ => return Err(Error::InvalidAddress),
}
self.inputs.push(TransparentInputInfo {
sk,
pubkey,
utxo,
coin,
});
Ok(())
}
pub fn add_output(&mut self, to: &TransparentAddress, value: Amount) -> Result<(), Error> {
if value.is_negative() {
return Err(Error::InvalidAmount);
}
self.vout.push(TxOut {
value,
script_pubkey: to.script(),
});
Ok(())
}
pub fn value_balance(&self) -> Option<Amount> {
#[cfg(feature = "transparent-inputs")]
let input_sum = self
.inputs
.iter()
.map(|input| input.coin.value)
.sum::<Option<Amount>>()?;
#[cfg(not(feature = "transparent-inputs"))]
let input_sum = Amount::zero();
input_sum
- self
.vout
.iter()
.map(|vo| vo.value)
.sum::<Option<Amount>>()?
}
pub fn build(&self) -> (Vec<TxIn>, Vec<TxOut>) {
#[cfg(feature = "transparent-inputs")]
let vin = self
.inputs
.iter()
.map(|i| TxIn::new(i.utxo.clone()))
.collect();
#[cfg(not(feature = "transparent-inputs"))]
let vin = vec![];
(vin, self.vout.clone())
}
#[cfg(feature = "transparent-inputs")]
pub fn create_signatures(
self,
mtx: &TransactionData,
consensus_branch_id: consensus::BranchId,
) -> Vec<Script> {
self.inputs
.iter()
.enumerate()
.map(|(i, info)| {
let mut sighash = [0u8; 32];
sighash.copy_from_slice(&signature_hash_data(
mtx,
consensus_branch_id,
SIGHASH_ALL,
SignableInput::transparent(i, &info.coin.script_pubkey, info.coin.value),
));
let msg = secp256k1::Message::from_slice(sighash.as_ref()).expect("32 bytes");
let sig = self.secp.sign(&msg, &info.sk);
// Signature has to have "SIGHASH_ALL" appended to it
let mut sig_bytes: Vec<u8> = sig.serialize_der()[..].to_vec();
sig_bytes.extend(&[SIGHASH_ALL as u8]);
// P2PKH scriptSig
Script::default() << &sig_bytes[..] << &info.pubkey[..]
})
.collect()
}
}