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,
script::{PushBytesBuf, ScriptBuf},
transaction::{Version, Transaction},
OutPoint, Sequence, Witness, TxIn, Amount, TxOut, Address,
OutPoint, Sequence, Witness, TxIn, Amount, TxOut,
};
use crate::{
@ -61,7 +61,11 @@ pub struct 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
let mut tx = Transaction {
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
.map(|payment| TxOut {
value: Amount::from_sat(payment.1),
script_pubkey: payment.0.script_pubkey(),
script_pubkey: payment.0.clone(),
})
.collect(),
};
if let Some(change) = change {
// 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)
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())
}
@ -121,8 +125,8 @@ impl SignableTransaction {
/// If data is specified, an OP_RETURN output will be added with it.
pub fn new(
mut inputs: Vec<ReceivedOutput>,
payments: &[(Address, u64)],
change: Option<&Address>,
payments: &[(ScriptBuf, u64)],
change: Option<ScriptBuf>,
data: Option<Vec<u8>>,
fee_per_weight: u64,
) -> Result<SignableTransaction, TransactionError> {
@ -159,10 +163,7 @@ impl SignableTransaction {
let payment_sat = payments.iter().map(|payment| payment.1).sum::<u64>();
let mut tx_outs = payments
.iter()
.map(|payment| TxOut {
value: Amount::from_sat(payment.1),
script_pubkey: payment.0.script_pubkey(),
})
.map(|payment| TxOut { value: Amount::from_sat(payment.1), script_pubkey: payment.0.clone() })
.collect::<Vec<_>>();
// 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 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;
if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) {
if value >= DUST {
tx_outs
.push(TxOut { value: Amount::from_sat(value), script_pubkey: change.script_pubkey() });
tx_outs.push(TxOut { value: Amount::from_sat(value), script_pubkey: change });
weight = weight_with_change;
needed_fee = fee_with_change;
}

View file

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

View file

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

View file

@ -135,7 +135,7 @@ mod bitcoin {
}],
output: vec![TxOut {
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,
network::Network,
WitnessVersion, WitnessProgram, ScriptBuf,
address::{AddressType, NetworkChecked, Address as BAddressGeneric},
address::{AddressType, NetworkChecked, Address as BAddress},
};
type BAddress = BAddressGeneric<NetworkChecked>;
#[derive(Clone, Eq, Debug)]
pub struct Address(BAddress);
pub struct Address(ScriptBuf);
impl PartialEq for Address {
fn eq(&self, other: &Self) -> bool {
// Since Serai defines the Bitcoin-address specification as a variant of 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 = ();
fn from_str(str: &str) -> Result<Address, ()> {
Address::new(
BAddressGeneric::from_str(str)
BAddress::from_str(str)
.map_err(|_| ())?
.require_network(Network::Bitcoin)
.map_err(|_| ())?,
.map_err(|_| ())?
.script_pubkey(),
)
.ok_or(())
}
@ -38,7 +43,9 @@ impl FromStr for Address {
impl fmt::Display for Address {
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, ()> {
Ok(Address(match EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())? {
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) => {
let script_hash = ScriptHash::from_raw_hash(Hash::from_byte_array(hash));
let res =
BAddress::from_script(&ScriptBuf::new_p2sh(&script_hash), Network::Bitcoin).unwrap();
debug_assert_eq!(res.script_hash(), Some(script_hash));
res
ScriptBuf::new_p2sh(&ScriptHash::from_raw_hash(Hash::from_byte_array(hash)))
}
EncodedAddress::P2WPKH(hash) => {
ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V0, &hash).unwrap())
}
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>, ()> {
let witness_program = |addr: &Address| {
let script = addr.0.script_pubkey();
let program_push = script.as_script().instructions().last().ok_or(())?.map_err(|_| ())?;
let program_push = addr.0.as_script().instructions().last().ok_or(())?.map_err(|_| ())?;
let program = program_push.push_bytes().ok_or(())?.as_bytes();
Ok::<_, ()>(program.to_vec())
};
let parsed_addr =
BAddress::<NetworkChecked>::from_script(&addr.0, Network::Bitcoin).map_err(|_| ())?;
Ok(
(match addr.0.address_type() {
(match parsed_addr.address_type() {
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) => {
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) => {
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 {
pub fn new(address: BAddress) -> Option<Self> {
pub fn new(address: ScriptBuf) -> Option<Self> {
let res = Self(address);
if try_to_vec(&res).is_ok() {
return Some(res);

View file

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

View file

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