Use ScriptBuf over Address where possible

This commit is contained in:
Luke Parker 2024-05-21 06:44:59 -04:00
parent 400319cd29
commit f93214012d
No known key found for this signature in database
7 changed files with 80 additions and 102 deletions

View file

@ -18,7 +18,7 @@ use bitcoin::{
absolute::LockTime, absolute::LockTime,
script::{PushBytesBuf, ScriptBuf}, script::{PushBytesBuf, ScriptBuf},
transaction::{Version, Transaction}, transaction::{Version, Transaction},
OutPoint, Sequence, Witness, TxIn, Amount, TxOut, Address, OutPoint, Sequence, Witness, TxIn, Amount, TxOut,
}; };
use crate::{ use crate::{
@ -61,7 +61,11 @@ pub struct SignableTransaction {
} }
impl SignableTransaction { impl SignableTransaction {
fn calculate_weight(inputs: usize, payments: &[(Address, u64)], change: Option<&Address>) -> u64 { fn calculate_weight(
inputs: usize,
payments: &[(ScriptBuf, u64)],
change: Option<&ScriptBuf>,
) -> u64 {
// Expand this a full transaction in order to use the bitcoin library's weight function // Expand this a full transaction in order to use the bitcoin library's weight function
let mut tx = Transaction { let mut tx = Transaction {
version: Version(2), version: Version(2),
@ -86,14 +90,14 @@ impl SignableTransaction {
// The script pub key is not of a fixed size and does have to be used here // The script pub key is not of a fixed size and does have to be used here
.map(|payment| TxOut { .map(|payment| TxOut {
value: Amount::from_sat(payment.1), value: Amount::from_sat(payment.1),
script_pubkey: payment.0.script_pubkey(), script_pubkey: payment.0.clone(),
}) })
.collect(), .collect(),
}; };
if let Some(change) = change { if let Some(change) = change {
// Use a 0 value since we're currently unsure what the change amount will be, and since // Use a 0 value since we're currently unsure what the change amount will be, and since
// the value is fixed size (so any value could be used here) // the value is fixed size (so any value could be used here)
tx.output.push(TxOut { value: Amount::ZERO, script_pubkey: change.script_pubkey() }); tx.output.push(TxOut { value: Amount::ZERO, script_pubkey: change.clone() });
} }
u64::from(tx.weight()) u64::from(tx.weight())
} }
@ -121,8 +125,8 @@ impl SignableTransaction {
/// If data is specified, an OP_RETURN output will be added with it. /// If data is specified, an OP_RETURN output will be added with it.
pub fn new( pub fn new(
mut inputs: Vec<ReceivedOutput>, mut inputs: Vec<ReceivedOutput>,
payments: &[(Address, u64)], payments: &[(ScriptBuf, u64)],
change: Option<&Address>, change: Option<ScriptBuf>,
data: Option<Vec<u8>>, data: Option<Vec<u8>>,
fee_per_weight: u64, fee_per_weight: u64,
) -> Result<SignableTransaction, TransactionError> { ) -> Result<SignableTransaction, TransactionError> {
@ -159,10 +163,7 @@ impl SignableTransaction {
let payment_sat = payments.iter().map(|payment| payment.1).sum::<u64>(); let payment_sat = payments.iter().map(|payment| payment.1).sum::<u64>();
let mut tx_outs = payments let mut tx_outs = payments
.iter() .iter()
.map(|payment| TxOut { .map(|payment| TxOut { value: Amount::from_sat(payment.1), script_pubkey: payment.0.clone() })
value: Amount::from_sat(payment.1),
script_pubkey: payment.0.script_pubkey(),
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Add the OP_RETURN output // Add the OP_RETURN output
@ -213,12 +214,11 @@ impl SignableTransaction {
// If there's a change address, check if there's change to give it // If there's a change address, check if there's change to give it
if let Some(change) = change { if let Some(change) = change {
let weight_with_change = Self::calculate_weight(tx_ins.len(), payments, Some(change)); let weight_with_change = Self::calculate_weight(tx_ins.len(), payments, Some(&change));
let fee_with_change = fee_per_weight * weight_with_change; let fee_with_change = fee_per_weight * weight_with_change;
if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) { if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) {
if value >= DUST { if value >= DUST {
tx_outs tx_outs.push(TxOut { value: Amount::from_sat(value), script_pubkey: change });
.push(TxOut { value: Amount::from_sat(value), script_pubkey: change.script_pubkey() });
weight = weight_with_change; weight = weight_with_change;
needed_fee = fee_with_change; needed_fee = fee_with_change;
} }

View file

@ -192,7 +192,7 @@ async_sequential! {
assert_eq!(output.offset(), Scalar::ZERO); assert_eq!(output.offset(), Scalar::ZERO);
let inputs = vec![output]; let inputs = vec![output];
let addr = || Address::from_script(&p2tr_script_buf(key).unwrap(), Network::Regtest).unwrap(); let addr = || p2tr_script_buf(key).unwrap();
let payments = vec![(addr(), 1000)]; let payments = vec![(addr(), 1000)];
assert!(SignableTransaction::new(inputs.clone(), &payments, None, None, FEE).is_ok()); assert!(SignableTransaction::new(inputs.clone(), &payments, None, None, FEE).is_ok());
@ -205,7 +205,7 @@ async_sequential! {
// No change // No change
assert!(SignableTransaction::new(inputs.clone(), &[(addr(), 1000)], None, None, FEE).is_ok()); assert!(SignableTransaction::new(inputs.clone(), &[(addr(), 1000)], None, None, FEE).is_ok());
// Consolidation TX // Consolidation TX
assert!(SignableTransaction::new(inputs.clone(), &[], Some(&addr()), None, FEE).is_ok()); assert!(SignableTransaction::new(inputs.clone(), &[], Some(addr()), None, FEE).is_ok());
// Data // Data
assert!(SignableTransaction::new(inputs.clone(), &[], None, Some(vec![]), FEE).is_ok()); assert!(SignableTransaction::new(inputs.clone(), &[], None, Some(vec![]), FEE).is_ok());
// No outputs // No outputs
@ -228,7 +228,7 @@ async_sequential! {
); );
assert_eq!( assert_eq!(
SignableTransaction::new(inputs.clone(), &[], Some(&addr()), None, 0), SignableTransaction::new(inputs.clone(), &[], Some(addr()), None, 0),
Err(TransactionError::TooLowFee), Err(TransactionError::TooLowFee),
); );
@ -260,20 +260,19 @@ async_sequential! {
// Declare payments, change, fee // Declare payments, change, fee
let payments = [ let payments = [
(Address::from_script(&p2tr_script_buf(key).unwrap(), Network::Regtest).unwrap(), 1005), (p2tr_script_buf(key).unwrap(), 1005),
(Address::from_script(&p2tr_script_buf(offset_key).unwrap(), Network::Regtest).unwrap(), 1007) (p2tr_script_buf(offset_key).unwrap(), 1007)
]; ];
let change_offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap(); let change_offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
let change_key = key + (ProjectivePoint::GENERATOR * change_offset); let change_key = key + (ProjectivePoint::GENERATOR * change_offset);
let change_addr = let change_addr = p2tr_script_buf(change_key).unwrap();
Address::from_script(&p2tr_script_buf(change_key).unwrap(), Network::Regtest).unwrap();
// Create and sign the TX // Create and sign the TX
let tx = SignableTransaction::new( let tx = SignableTransaction::new(
vec![output.clone(), offset_output.clone()], vec![output.clone(), offset_output.clone()],
&payments, &payments,
Some(&change_addr), Some(change_addr.clone()),
None, None,
FEE FEE
).unwrap(); ).unwrap();
@ -298,7 +297,7 @@ async_sequential! {
for ((output, scanned), payment) in tx.output.iter().zip(outputs.iter()).zip(payments.iter()) { for ((output, scanned), payment) in tx.output.iter().zip(outputs.iter()).zip(payments.iter()) {
assert_eq!( assert_eq!(
output, output,
&TxOut { script_pubkey: payment.0.script_pubkey(), value: Amount::from_sat(payment.1) }, &TxOut { script_pubkey: payment.0.clone(), value: Amount::from_sat(payment.1) },
); );
assert_eq!(scanned.value(), payment.1 ); assert_eq!(scanned.value(), payment.1 );
} }
@ -313,7 +312,7 @@ async_sequential! {
input_value - payments.iter().map(|payment| payment.1).sum::<u64>() - needed_fee; input_value - payments.iter().map(|payment| payment.1).sum::<u64>() - needed_fee;
assert_eq!( assert_eq!(
tx.output[2], tx.output[2],
TxOut { script_pubkey: change_addr.script_pubkey(), value: Amount::from_sat(change_amount) }, TxOut { script_pubkey: change_addr, value: Amount::from_sat(change_amount) },
); );
// This also tests send_raw_transaction and get_transaction, which the RPC test can't // This also tests send_raw_transaction and get_transaction, which the RPC test can't
@ -343,7 +342,7 @@ async_sequential! {
&SignableTransaction::new( &SignableTransaction::new(
vec![output], vec![output],
&[], &[],
Some(&Address::from_script(&p2tr_script_buf(key).unwrap(), Network::Regtest).unwrap()), Some(p2tr_script_buf(key).unwrap()),
Some(data.clone()), Some(data.clone()),
FEE FEE
).unwrap() ).unwrap()

View file

@ -20,8 +20,7 @@ use bitcoin_serai::{
key::{Parity, XOnlyPublicKey}, key::{Parity, XOnlyPublicKey},
consensus::{Encodable, Decodable}, consensus::{Encodable, Decodable},
script::Instruction, script::Instruction,
address::Address as BAddress, Transaction, Block, ScriptBuf,
Transaction, Block, Network as BNetwork, ScriptBuf,
opcodes::all::{OP_SHA256, OP_EQUALVERIFY}, opcodes::all::{OP_SHA256, OP_EQUALVERIFY},
}, },
wallet::{ wallet::{
@ -454,7 +453,7 @@ impl Bitcoin {
match BSignableTransaction::new( match BSignableTransaction::new(
inputs.iter().map(|input| input.output.clone()).collect(), inputs.iter().map(|input| input.output.clone()).collect(),
&payments, &payments,
change.as_ref().map(AsRef::as_ref), change.clone().map(Into::into),
None, None,
fee.0, fee.0,
) { ) {
@ -535,6 +534,8 @@ impl Bitcoin {
input_index: usize, input_index: usize,
private_key: &PrivateKey, private_key: &PrivateKey,
) -> ScriptBuf { ) -> ScriptBuf {
use bitcoin_serai::bitcoin::{Network as BNetwork, Address as BAddress};
let public_key = PublicKey::from_private_key(SECP256K1, private_key); let public_key = PublicKey::from_private_key(SECP256K1, private_key);
let main_addr = BAddress::p2pkh(public_key, BNetwork::Regtest); let main_addr = BAddress::p2pkh(public_key, BNetwork::Regtest);
@ -581,13 +582,9 @@ const MAX_OUTPUTS: usize = 520;
fn address_from_key(key: ProjectivePoint) -> Address { fn address_from_key(key: ProjectivePoint) -> Address {
Address::new( Address::new(
BAddress::from_script( p2tr_script_buf(key).expect("creating address from key which isn't properly tweaked"),
&p2tr_script_buf(key).expect("creating address from key which isn't properly tweaked"),
BNetwork::Bitcoin,
)
.expect("couldn't go from p2tr script buf to address"),
) )
.expect("couldn't create Serai-representable address for bitcoin address") .expect("couldn't create Serai-representable address for P2TR script")
} }
#[async_trait] #[async_trait]
@ -733,9 +730,7 @@ impl Network for Bitcoin {
} }
tx.unwrap().output.swap_remove(usize::try_from(input.previous_output.vout).unwrap()) tx.unwrap().output.swap_remove(usize::try_from(input.previous_output.vout).unwrap())
}; };
BAddress::from_script(&spent_output.script_pubkey, BNetwork::Bitcoin) Address::new(spent_output.script_pubkey)
.ok()
.and_then(Address::new)
}; };
let data = Self::extract_serai_data(tx); let data = Self::extract_serai_data(tx);
for output in &mut outputs { for output in &mut outputs {
@ -903,6 +898,8 @@ impl Network for Bitcoin {
#[cfg(test)] #[cfg(test)]
async fn mine_block(&self) { async fn mine_block(&self) {
use bitcoin_serai::bitcoin::{Network as BNetwork, Address as BAddress};
self self
.rpc .rpc
.rpc_call::<Vec<String>>( .rpc_call::<Vec<String>>(
@ -915,6 +912,8 @@ impl Network for Bitcoin {
#[cfg(test)] #[cfg(test)]
async fn test_send(&self, address: Address) -> Block { async fn test_send(&self, address: Address) -> Block {
use bitcoin_serai::bitcoin::{Network as BNetwork, Address as BAddress};
let secret_key = SecretKey::new(&mut rand_core::OsRng); let secret_key = SecretKey::new(&mut rand_core::OsRng);
let private_key = PrivateKey::new(secret_key, BNetwork::Regtest); let private_key = PrivateKey::new(secret_key, BNetwork::Regtest);
let public_key = PublicKey::from_private_key(SECP256K1, &private_key); let public_key = PublicKey::from_private_key(SECP256K1, &private_key);
@ -939,7 +938,7 @@ impl Network for Bitcoin {
}], }],
output: vec![TxOut { output: vec![TxOut {
value: tx.output[0].value - BAmount::from_sat(10000), value: tx.output[0].value - BAmount::from_sat(10000),
script_pubkey: address.as_ref().script_pubkey(), script_pubkey: address.clone().into(),
}], }],
}; };
tx.input[0].script_sig = Self::sign_btc_input_for_p2pkh(&tx, 0, &private_key); tx.input[0].script_sig = Self::sign_btc_input_for_p2pkh(&tx, 0, &private_key);

View file

@ -135,7 +135,7 @@ mod bitcoin {
}], }],
output: vec![TxOut { output: vec![TxOut {
value: tx.output[0].value - BAmount::from_sat(10000), value: tx.output[0].value - BAmount::from_sat(10000),
script_pubkey: serai_btc_address.as_ref().script_pubkey(), script_pubkey: serai_btc_address.into(),
}], }],
}; };

View file

@ -7,19 +7,23 @@ use bitcoin::{
PubkeyHash, ScriptHash, PubkeyHash, ScriptHash,
network::Network, network::Network,
WitnessVersion, WitnessProgram, ScriptBuf, WitnessVersion, WitnessProgram, ScriptBuf,
address::{AddressType, NetworkChecked, Address as BAddressGeneric}, address::{AddressType, NetworkChecked, Address as BAddress},
}; };
type BAddress = BAddressGeneric<NetworkChecked>;
#[derive(Clone, Eq, Debug)] #[derive(Clone, Eq, Debug)]
pub struct Address(BAddress); pub struct Address(ScriptBuf);
impl PartialEq for Address { impl PartialEq for Address {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
// Since Serai defines the Bitcoin-address specification as a variant of the script alone, // Since Serai defines the Bitcoin-address specification as a variant of the script alone,
// define equivalency as the script alone // define equivalency as the script alone
self.0.script_pubkey() == other.0.script_pubkey() self.0 == other.0
}
}
impl From<Address> for ScriptBuf {
fn from(addr: Address) -> ScriptBuf {
addr.0
} }
} }
@ -27,10 +31,11 @@ impl FromStr for Address {
type Err = (); type Err = ();
fn from_str(str: &str) -> Result<Address, ()> { fn from_str(str: &str) -> Result<Address, ()> {
Address::new( Address::new(
BAddressGeneric::from_str(str) BAddress::from_str(str)
.map_err(|_| ())? .map_err(|_| ())?
.require_network(Network::Bitcoin) .require_network(Network::Bitcoin)
.map_err(|_| ())?, .map_err(|_| ())?
.script_pubkey(),
) )
.ok_or(()) .ok_or(())
} }
@ -38,7 +43,9 @@ impl FromStr for Address {
impl fmt::Display for Address { impl fmt::Display for Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f) BAddress::<NetworkChecked>::from_script(&self.0, Network::Bitcoin)
.map_err(|_| fmt::Error)?
.fmt(f)
} }
} }
@ -57,45 +64,40 @@ impl TryFrom<Vec<u8>> for Address {
fn try_from(data: Vec<u8>) -> Result<Address, ()> { fn try_from(data: Vec<u8>) -> Result<Address, ()> {
Ok(Address(match EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())? { Ok(Address(match EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())? {
EncodedAddress::P2PKH(hash) => { EncodedAddress::P2PKH(hash) => {
BAddress::p2pkh(PubkeyHash::from_raw_hash(Hash::from_byte_array(hash)), Network::Bitcoin) ScriptBuf::new_p2pkh(&PubkeyHash::from_raw_hash(Hash::from_byte_array(hash)))
} }
EncodedAddress::P2SH(hash) => { EncodedAddress::P2SH(hash) => {
let script_hash = ScriptHash::from_raw_hash(Hash::from_byte_array(hash)); ScriptBuf::new_p2sh(&ScriptHash::from_raw_hash(Hash::from_byte_array(hash)))
let res = }
BAddress::from_script(&ScriptBuf::new_p2sh(&script_hash), Network::Bitcoin).unwrap(); EncodedAddress::P2WPKH(hash) => {
debug_assert_eq!(res.script_hash(), Some(script_hash)); ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V0, &hash).unwrap())
res }
EncodedAddress::P2WSH(hash) => {
ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V0, &hash).unwrap())
}
EncodedAddress::P2TR(key) => {
ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V1, &key).unwrap())
} }
EncodedAddress::P2WPKH(hash) => BAddress::from_witness_program(
WitnessProgram::new(WitnessVersion::V0, &hash).unwrap(),
Network::Bitcoin,
),
EncodedAddress::P2WSH(hash) => BAddress::from_witness_program(
WitnessProgram::new(WitnessVersion::V0, &hash).unwrap(),
Network::Bitcoin,
),
EncodedAddress::P2TR(key) => BAddress::from_witness_program(
WitnessProgram::new(WitnessVersion::V1, &key).unwrap(),
Network::Bitcoin,
),
})) }))
} }
} }
fn try_to_vec(addr: &Address) -> Result<Vec<u8>, ()> { fn try_to_vec(addr: &Address) -> Result<Vec<u8>, ()> {
let witness_program = |addr: &Address| { let witness_program = |addr: &Address| {
let script = addr.0.script_pubkey(); let program_push = addr.0.as_script().instructions().last().ok_or(())?.map_err(|_| ())?;
let program_push = script.as_script().instructions().last().ok_or(())?.map_err(|_| ())?;
let program = program_push.push_bytes().ok_or(())?.as_bytes(); let program = program_push.push_bytes().ok_or(())?.as_bytes();
Ok::<_, ()>(program.to_vec()) Ok::<_, ()>(program.to_vec())
}; };
let parsed_addr =
BAddress::<NetworkChecked>::from_script(&addr.0, Network::Bitcoin).map_err(|_| ())?;
Ok( Ok(
(match addr.0.address_type() { (match parsed_addr.address_type() {
Some(AddressType::P2pkh) => { Some(AddressType::P2pkh) => {
EncodedAddress::P2PKH(*addr.0.pubkey_hash().unwrap().as_raw_hash().as_byte_array()) EncodedAddress::P2PKH(*parsed_addr.pubkey_hash().unwrap().as_raw_hash().as_byte_array())
} }
Some(AddressType::P2sh) => { Some(AddressType::P2sh) => {
EncodedAddress::P2SH(*addr.0.script_hash().unwrap().as_raw_hash().as_byte_array()) EncodedAddress::P2SH(*parsed_addr.script_hash().unwrap().as_raw_hash().as_byte_array())
} }
Some(AddressType::P2wpkh) => { Some(AddressType::P2wpkh) => {
let program = witness_program(addr)?; let program = witness_program(addr)?;
@ -127,20 +129,8 @@ impl From<Address> for Vec<u8> {
} }
} }
impl From<Address> for BAddress {
fn from(addr: Address) -> BAddress {
addr.0
}
}
impl AsRef<BAddress> for Address {
fn as_ref(&self) -> &BAddress {
&self.0
}
}
impl Address { impl Address {
pub fn new(address: BAddress) -> Option<Self> { pub fn new(address: ScriptBuf) -> Option<Self> {
let res = Self(address); let res = Self(address);
if try_to_vec(&res).is_ok() { if try_to_vec(&res).is_ok() {
return Some(res); return Some(res);

View file

@ -454,19 +454,17 @@ async fn mint_and_burn_test() {
// Create a random Bitcoin/Monero address // Create a random Bitcoin/Monero address
let bitcoin_addr = { let bitcoin_addr = {
use bitcoin_serai::bitcoin::{network::Network, key::PublicKey, address::Address}; use bitcoin_serai::bitcoin::{key::PublicKey, ScriptBuf};
// Uses Network::Bitcoin since it doesn't actually matter, Serai strips it out ScriptBuf::new_p2pkh(
// TODO: Move Serai to ScriptBuf from Address &(loop {
Address::p2pkh(
loop {
let mut bytes = [0; 33]; let mut bytes = [0; 33];
OsRng.fill_bytes(&mut bytes); OsRng.fill_bytes(&mut bytes);
bytes[0] %= 4; bytes[0] %= 4;
if let Ok(key) = PublicKey::from_slice(&bytes) { if let Ok(key) = PublicKey::from_slice(&bytes) {
break key; break key;
} }
}, })
Network::Bitcoin, .pubkey_hash(),
) )
}; };
@ -559,7 +557,7 @@ async fn mint_and_burn_test() {
let received_output = block.txdata[1] let received_output = block.txdata[1]
.output .output
.iter() .iter()
.find(|output| output.script_pubkey == bitcoin_addr.script_pubkey()) .find(|output| output.script_pubkey == bitcoin_addr)
.unwrap(); .unwrap();
let tx_fee = 1_100_000_00 - let tx_fee = 1_100_000_00 -

View file

@ -261,7 +261,6 @@ impl Wallet {
OutPoint, Sequence, Witness, TxIn, Amount, TxOut, OutPoint, Sequence, Witness, TxIn, Amount, TxOut,
absolute::LockTime, absolute::LockTime,
transaction::{Version, Transaction}, transaction::{Version, Transaction},
Network, Address,
}; };
const AMOUNT: u64 = 100000000; const AMOUNT: u64 = 100000000;
@ -281,13 +280,11 @@ impl Wallet {
}, },
TxOut { TxOut {
value: Amount::from_sat(AMOUNT), value: Amount::from_sat(AMOUNT),
script_pubkey: Address::p2tr_tweaked( script_pubkey: ScriptBuf::new_p2tr_tweaked(
TweakedPublicKey::dangerous_assume_tweaked( TweakedPublicKey::dangerous_assume_tweaked(
XOnlyPublicKey::from_slice(&to[1 ..]).unwrap(), XOnlyPublicKey::from_slice(&to[1 ..]).unwrap(),
), ),
Network::Bitcoin, ),
)
.script_pubkey(),
}, },
], ],
}; };
@ -521,13 +518,8 @@ impl Wallet {
match self { match self {
Wallet::Bitcoin { public_key, .. } => { Wallet::Bitcoin { public_key, .. } => {
use bitcoin_serai::bitcoin::{Network, Address}; use bitcoin_serai::bitcoin::ScriptBuf;
ExternalAddress::new( ExternalAddress::new(ScriptBuf::new_p2pkh(&public_key.pubkey_hash()).into()).unwrap()
networks::bitcoin::Address::new(Address::p2pkh(public_key, Network::Regtest))
.unwrap()
.into(),
)
.unwrap()
} }
Wallet::Ethereum { key, .. } => ExternalAddress::new( Wallet::Ethereum { key, .. } => ExternalAddress::new(
ethereum_serai::crypto::address(&(ciphersuite::Secp256k1::generator() * key)).into(), ethereum_serai::crypto::address(&(ciphersuite::Secp256k1::generator() * key)).into(),