From 26397bf0e98a5352fdad4abc54e4a147bcbb61e8 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 26 Dec 2023 04:39:46 +0800 Subject: [PATCH] Allow specifying destination for unallocated runes (#2899) --- src/index/updater/rune_updater.rs | 24 ++- src/runes.rs | 265 ++++++++++++++++++++++++++++++ src/runes/runestone.rs | 15 +- src/subcommand/wallet/etch.rs | 1 + tests/wallet/send.rs | 1 + 5 files changed, 299 insertions(+), 7 deletions(-) diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 54b7c7446a..3ce5d298cb 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -63,6 +63,12 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { .map(|runestone| runestone.burn) .unwrap_or_default(); + let default_output = runestone.as_ref().and_then(|runestone| { + runestone + .default_output + .and_then(|default| usize::try_from(default).ok()) + }); + // A vector of allocated transaction output rune balances let mut allocated: Vec> = vec![HashMap::new(); tx.output.len()]; @@ -327,12 +333,18 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { *burned.entry(id).or_default() += balance; } } else { - // Assign all un-allocated runes to the first non OP_RETURN output - if let Some((vout, _)) = tx - .output - .iter() - .enumerate() - .find(|(_, tx_out)| !tx_out.script_pubkey.is_op_return()) + // assign all un-allocated runes to the default output, or the first non + // OP_RETURN output if there is no default, or if the default output is + // too large + if let Some(vout) = default_output + .filter(|vout| *vout < allocated.len()) + .or_else(|| { + tx.output + .iter() + .enumerate() + .find(|(_vout, tx_out)| !tx_out.script_pubkey.is_op_return()) + .map(|(vout, _tx_out)| vout) + }) { for (id, balance) in unallocated { if balance > 0 { diff --git a/src/runes.rs b/src/runes.rs index 4ce5ea9f89..aa068d9138 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -892,6 +892,7 @@ mod tests { rune: Some(Rune(RUNE)), ..Default::default() }), + default_output: None, burn: true, } .encipher(), @@ -944,6 +945,7 @@ mod tests { term: Some(1), spacers: 1, }), + default_output: None, burn: true, } .encipher(), @@ -997,6 +999,7 @@ mod tests { }], etching: Some(Etching::default()), burn: true, + default_output: None, } .encipher(), ), @@ -1267,6 +1270,268 @@ mod tests { ); } + #[test] + fn unallocated_runes_are_assigned_to_default_output() { + let context = Context::builder().arg("--index-runes").build(); + + context.mine_blocks(1); + + let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + context.assert_runes( + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + timestamp: 2, + ..Default::default() + }, + )], + [( + OutPoint { + txid: txid0, + vout: 0, + }, + vec![(id, u128::max_value())], + )], + ); + + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Witness::new())], + outputs: 2, + op_return: Some( + Runestone { + default_output: Some(1), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + context.assert_runes( + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + timestamp: 2, + ..Default::default() + }, + )], + [( + OutPoint { + txid: txid1, + vout: 1, + }, + vec![(id, u128::max_value())], + )], + ); + } + + #[test] + fn unallocated_runes_are_assigned_to_first_non_op_return_output_if_default_is_too_large() { + let context = Context::builder().arg("--index-runes").build(); + + context.mine_blocks(1); + + let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + context.assert_runes( + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + timestamp: 2, + ..Default::default() + }, + )], + [( + OutPoint { + txid: txid0, + vout: 0, + }, + vec![(id, u128::max_value())], + )], + ); + + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Witness::new())], + outputs: 2, + op_return: Some( + Runestone { + default_output: Some(3), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + context.assert_runes( + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + timestamp: 2, + ..Default::default() + }, + )], + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, u128::max_value())], + )], + ); + } + + #[test] + fn unallocated_runes_are_burned_if_default_output_is_op_return() { + let context = Context::builder().arg("--index-runes").build(); + + context.mine_blocks(1); + + let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + context.assert_runes( + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + timestamp: 2, + ..Default::default() + }, + )], + [( + OutPoint { + txid: txid0, + vout: 0, + }, + vec![(id, u128::max_value())], + )], + ); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Witness::new())], + outputs: 2, + op_return: Some( + Runestone { + default_output: Some(2), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + context.assert_runes( + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + burned: u128::max_value(), + timestamp: 2, + ..Default::default() + }, + )], + [], + ); + } + #[test] fn unallocated_runes_in_transactions_with_no_runestone_are_assigned_to_first_non_op_return_output( ) { diff --git a/src/runes/runestone.rs b/src/runes/runestone.rs index 45b05a183a..e1cf9aa63e 100644 --- a/src/runes/runestone.rs +++ b/src/runes/runestone.rs @@ -6,6 +6,7 @@ const TAG_RUNE: u128 = 4; const TAG_LIMIT: u128 = 6; const TAG_TERM: u128 = 8; const TAG_DEADLINE: u128 = 10; +const TAG_DEFAULT_OUTPUT: u128 = 12; const TAG_DIVISIBILITY: u128 = 1; const TAG_SPACERS: u128 = 3; @@ -25,6 +26,7 @@ const MAX_SPACERS: u32 = 0b00000111_11111111_11111111_11111111; pub struct Runestone { pub edicts: Vec, pub etching: Option, + pub default_output: Option, pub burn: bool, } @@ -87,6 +89,7 @@ impl Runestone { let spacers = fields.remove(&TAG_SPACERS); let symbol = fields.remove(&TAG_SYMBOL); let term = fields.remove(&TAG_TERM); + let default_output = fields.remove(&TAG_DEFAULT_OUTPUT); let etch = flags & FLAG_ETCH != 0; let unrecognized_flags = flags & !FLAG_ETCH != 0; @@ -114,9 +117,10 @@ impl Runestone { }; Ok(Some(Self { + burn: unrecognized_flags || fields.keys().any(|tag| tag % 2 == 0), + default_output: default_output.and_then(|default| u32::try_from(default).ok()), edicts: body, etching, - burn: unrecognized_flags || fields.keys().any(|tag| tag % 2 == 0), })) } @@ -163,6 +167,11 @@ impl Runestone { } } + if let Some(default_output) = self.default_output { + varint::encode_to_vec(TAG_DEFAULT_OUTPUT, &mut payload); + varint::encode_to_vec(default_output.into(), &mut payload); + } + if self.burn { varint::encode_to_vec(TAG_BURN, &mut payload); varint::encode_to_vec(0, &mut payload); @@ -863,6 +872,7 @@ mod tests { output: 3, }], etching: None, + default_output: None, burn: false, }, ); @@ -1431,6 +1441,7 @@ mod tests { output: 7, }, ], + default_output: Some(11), burn: false, }, &[ @@ -1450,6 +1461,8 @@ mod tests { 3, TAG_TERM, 5, + TAG_DEFAULT_OUTPUT, + 11, TAG_BODY, 6, 5, diff --git a/src/subcommand/wallet/etch.rs b/src/subcommand/wallet/etch.rs index 98365c1e5f..6c02fe06ac 100644 --- a/src/subcommand/wallet/etch.rs +++ b/src/subcommand/wallet/etch.rs @@ -75,6 +75,7 @@ impl Etch { id: 0, output: 1, }], + default_output: None, burn: false, }; diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index cdbe0c22b0..cb17c44497 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -848,6 +848,7 @@ fn sending_rune_creates_transaction_with_expected_runestone() { assert_eq!( Runestone::from_transaction(&tx).unwrap(), Runestone { + default_output: None, etching: None, edicts: vec![Edict { id: RuneId {