Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

align ordinal to be first in recipient output #666

Merged
merged 3 commits into from
Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/subcommand/wallet/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ impl Send {

let utxos = list_unspent(&options, &index)?.into_iter().collect();

let change = [
let change = vec![
client
.call("getrawchangeaddress", &[])
.context("Could not get change addresses from wallet")?,
Expand Down
174 changes: 134 additions & 40 deletions src/subcommand/wallet/transaction_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ impl std::error::Error for Error {}

#[derive(Debug, PartialEq)]
pub(crate) struct TransactionBuilder {
change: [Address; 2],
change: Vec<Address>,
inputs: Vec<OutPoint>,
ordinal: Ordinal,
outputs: Vec<(Address, Amount)>,
Expand All @@ -70,7 +70,7 @@ impl TransactionBuilder {
ranges: BTreeMap<OutPoint, Vec<(u64, u64)>>,
ordinal: Ordinal,
recipient: Address,
change: [Address; 2],
change: Vec<Address>,
) -> Result<Transaction> {
Self::new(ranges, ordinal, recipient, change)
.select_ordinal()?
Expand All @@ -84,7 +84,7 @@ impl TransactionBuilder {
ranges: BTreeMap<OutPoint, Vec<(u64, u64)>>,
ordinal: Ordinal,
recipient: Address,
change: [Address; 2],
change: Vec<Address>,
) -> Self {
Self {
utxos: ranges.keys().cloned().collect(),
Expand Down Expand Up @@ -128,38 +128,39 @@ impl TransactionBuilder {
);

let ordinal_offset = self.calculate_ordinal_offset();
let (ordinal_address, ordinal_amount) = self.outputs[0].clone();

if ordinal_offset != 0 {
self.outputs[0] = (self.change[0].clone(), Amount::from_sat(ordinal_offset));
self.outputs.push((
ordinal_address,
ordinal_amount - Amount::from_sat(ordinal_offset),
));
self.outputs.insert(
0,
(
self.change.pop().expect("not enough change addresses"),
Amount::from_sat(ordinal_offset),
),
);
self.outputs.last_mut().expect("no output").1 -= Amount::from_sat(ordinal_offset);
}

Ok(self)
}

fn strip_excess_postage(mut self) -> Result<Self> {
let ordinal_offset = self.calculate_ordinal_offset();
let output_total = self
let total_output_amount = self
.outputs
.iter()
.map(|(_address, amount)| *amount)
.sum::<Amount>();

let ordinal_output_idx = self
let _ordinal_output_idx = self
.outputs
.iter()
.position(|(address, _amount)| address == &self.recipient)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let _ordinal_output_idx = self
.outputs
.iter()
.position(|(address, _amount)| address == &self.recipient)
self
.outputs
.iter()
.find(|(address, _amount)| address == &self.recipient)

.unwrap();
.expect("couldn't find output that contains the index");

if output_total > Amount::from_sat(ordinal_offset + Self::MAX_POSTAGE) {
self.outputs[ordinal_output_idx].1 = Amount::from_sat(Self::TARGET_POSTAGE);
if total_output_amount > Amount::from_sat(ordinal_offset + Self::MAX_POSTAGE) {
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
self.outputs.last_mut().expect("no outputs found").1 = Amount::from_sat(Self::TARGET_POSTAGE);
self.outputs.push((
self.change[1].clone(),
output_total - Amount::from_sat(ordinal_offset + Self::TARGET_POSTAGE),
self.change.pop().expect("not enough change addresses"),
total_output_amount - Amount::from_sat(ordinal_offset + Self::TARGET_POSTAGE),
raphjaph marked this conversation as resolved.
Show resolved Hide resolved
));
}

Expand All @@ -172,18 +173,19 @@ impl TransactionBuilder {
let tx = self.build()?;
let fee = Amount::from_sat((Self::FEE_RATE * tx.vsize()).try_into().unwrap());

let output_amount = self
let total_output_amount = self
.outputs
.iter()
.map(|(_address, amount)| *amount)
.sum::<Amount>();

if output_amount - fee > Amount::from_sat(ordinal_offset) {
let (_address, amount) = self
.outputs
.last_mut()
.expect("No output to deduct fee from");
*amount -= fee;
let (_address, last_output_amount) = self
.outputs
.last_mut()
.expect("No output to deduct fee from");

if total_output_amount - fee > Amount::from_sat(ordinal_offset) && *last_output_amount >= fee {
*last_output_amount -= fee;
} else {
return Err(Error::ConsumedByFee(self.ordinal));
}
Expand All @@ -194,7 +196,6 @@ impl TransactionBuilder {
fn build(&self) -> Result<Transaction> {
let ordinal = self.ordinal.n();
let recipient = self.recipient.script_pubkey();

let transaction = Transaction {
version: 1,
lock_time: PackedLockTime::ZERO,
Expand Down Expand Up @@ -270,17 +271,19 @@ impl TransactionBuilder {
}
assert!(found, "invariant: ordinal is found in outputs");

let mut offset = 0;
for output in &transaction.output {
if !self
.change
.iter()
.any(|address| output.script_pubkey == address.script_pubkey())
{
if output.script_pubkey == self.recipient.script_pubkey() {
assert!(
output.value < Self::MAX_POSTAGE,
"invariant: excess postage is stripped"
);
assert_eq!(
offset, ordinal_offset,
"invariant: ordinal is at first position in recipient output"
);
}
offset += output.value;
}

Ok(transaction)
Expand Down Expand Up @@ -333,7 +336,7 @@ mod tests {
"tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse()
.unwrap(),
[
vec![
"tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww"
.parse()
.unwrap(),
Expand Down Expand Up @@ -398,7 +401,7 @@ mod tests {
recipient: "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse()
.unwrap(),
change: [
change: vec![
"tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww"
.parse()
.unwrap(),
Expand Down Expand Up @@ -513,7 +516,7 @@ mod tests {
"tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse()
.unwrap(),
[
vec![
"tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww"
.parse()
.unwrap(),
Expand Down Expand Up @@ -560,7 +563,7 @@ mod tests {
"tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse()
.unwrap(),
[
vec![
"tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww"
.parse()
.unwrap(),
Expand Down Expand Up @@ -589,7 +592,7 @@ mod tests {
"tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse()
.unwrap(),
[
vec![
"tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww"
.parse()
.unwrap(),
Expand Down Expand Up @@ -618,7 +621,7 @@ mod tests {
"tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse()
.unwrap(),
[
vec![
"tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww"
.parse()
.unwrap(),
Expand Down Expand Up @@ -647,7 +650,7 @@ mod tests {
"tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse()
.unwrap(),
[
vec![
"tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww"
.parse()
.unwrap(),
Expand Down Expand Up @@ -682,7 +685,7 @@ mod tests {
"tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse()
.unwrap(),
[
vec![
"tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww"
.parse()
.unwrap(),
Expand Down Expand Up @@ -715,7 +718,7 @@ mod tests {
"tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse()
.unwrap(),
[
vec![
"tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww"
.parse()
.unwrap(),
Expand Down Expand Up @@ -771,7 +774,7 @@ mod tests {
"tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse()
.unwrap(),
[
vec![
"tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww"
.parse()
.unwrap(),
Expand All @@ -785,4 +788,95 @@ mod tests {
.build()
.unwrap();
}

#[test]
fn ordinal_is_aligned() {
let utxos = vec![(
"1111111111111111111111111111111111111111111111111111111111111111:1"
.parse()
.unwrap(),
vec![(0, 10_000)],
)];

pretty_assert_eq!(
TransactionBuilder::build_transaction(
utxos.into_iter().collect(),
Ordinal(3333),
"tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse()
.unwrap(),
vec![
"tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww"
.parse()
.unwrap(),
"tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l"
.parse()
.unwrap(),
]
),
Ok(Transaction {
version: 1,
lock_time: PackedLockTime::ZERO,
input: vec![TxIn {
previous_output: "1111111111111111111111111111111111111111111111111111111111111111:1"
.parse()
.unwrap(),
script_sig: Script::new(),
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
witness: Witness::new(),
},],
output: vec![
TxOut {
value: 3333,
script_pubkey: "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l"
.parse::<Address>()
.unwrap()
.script_pubkey(),
},
TxOut {
value: 10_000 - 3333 - 113,
script_pubkey: "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse::<Address>()
.unwrap()
.script_pubkey(),
}
],
})
)
}

#[test]
#[should_panic(expected = "invariant: ordinal is at first position in recipient output")]
fn invariant_ordinal_is_aligned() {
let utxos = vec![(
"1111111111111111111111111111111111111111111111111111111111111111:1"
.parse()
.unwrap(),
vec![(0, 10_000)],
)];

TransactionBuilder::new(
utxos.into_iter().collect(),
Ordinal(3333),
"tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
.parse()
.unwrap(),
vec![
"tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww"
.parse()
.unwrap(),
"tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l"
.parse()
.unwrap(),
],
)
.select_ordinal()
.unwrap()
.strip_excess_postage()
.unwrap()
.deduct_fee()
.unwrap()
.build()
.unwrap();
}
}