From 2a0c05b1115964a35966eab182ad0a52fe5811a0 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 23 Dec 2023 06:55:18 -0800 Subject: [PATCH 1/8] Allow specifying default rune destination --- src/index/updater/rune_updater.rs | 23 ++- src/runes.rs | 265 ++++++++++++++++++++++++++++++ src/runes/runestone.rs | 15 +- src/subcommand/wallet/etch.rs | 1 + tests/wallet/send.rs | 1 + 5 files changed, 297 insertions(+), 8 deletions(-) diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index c7c37f6e5d..e352328ca6 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 = runestone.as_ref().and_then(|runestone| { + runestone + .default + .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()]; @@ -322,13 +328,16 @@ 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.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 { *allocated[vout].entry(id).or_default() += balance; diff --git a/src/runes.rs b/src/runes.rs index b7224fbccf..b86bd7b3d1 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -892,6 +892,7 @@ mod tests { rune: Some(Rune(RUNE)), ..Default::default() }), + default: None, burn: true, } .encipher(), @@ -944,6 +945,7 @@ mod tests { term: Some(1), spacers: 1, }), + default: None, burn: true, } .encipher(), @@ -997,6 +999,7 @@ mod tests { }], etching: Some(Etching::default()), burn: true, + default: 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: 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_not_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: 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: 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..fa949cd85e 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: 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: 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 = fields.remove(&TAG_DEFAULT); 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: default.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) = self.default { + varint::encode_to_vec(TAG_DEFAULT, &mut payload); + varint::encode_to_vec(default.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: None, burn: false, }, ); @@ -1431,6 +1441,7 @@ mod tests { output: 7, }, ], + default: Some(11), burn: false, }, &[ @@ -1450,6 +1461,8 @@ mod tests { 3, TAG_TERM, 5, + TAG_DEFAULT, + 11, TAG_BODY, 6, 5, diff --git a/src/subcommand/wallet/etch.rs b/src/subcommand/wallet/etch.rs index 98365c1e5f..b2f77fd97e 100644 --- a/src/subcommand/wallet/etch.rs +++ b/src/subcommand/wallet/etch.rs @@ -75,6 +75,7 @@ impl Etch { id: 0, output: 1, }], + default: None, burn: false, }; diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index cdbe0c22b0..69fbd0a485 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: None, etching: None, edicts: vec![Edict { id: RuneId { From a039f11213712617e66253c707f031edcb3ca11e Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 25 Dec 2023 12:25:59 -0800 Subject: [PATCH 2/8] Rename --- src/index/updater/rune_updater.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 22ff7a6735..a07f9dce43 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -63,7 +63,7 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { .map(|runestone| runestone.burn) .unwrap_or_default(); - let default = runestone.as_ref().and_then(|runestone| { + let default_output = runestone.as_ref().and_then(|runestone| { runestone .default .and_then(|default| usize::try_from(default).ok()) @@ -336,13 +336,16 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { // 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.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) - }) { + 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 { *allocated[vout].entry(id).or_default() += balance; From 69e526389acc7c8be2992e5b8d197aa8b52977ce Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 25 Dec 2023 12:28:59 -0800 Subject: [PATCH 3/8] Tweak --- src/runes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runes.rs b/src/runes.rs index 1c36808733..3c4eeeb0c6 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -1360,7 +1360,7 @@ mod tests { } #[test] - fn unallocated_runes_are_assigned_to_first_not_op_return_output_if_default_is_too_large() { + 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); From 40efd9885bc64cab76166692dd0fd6be27813291 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 25 Dec 2023 12:30:46 -0800 Subject: [PATCH 4/8] TAG_DEFAULT -> TAG_DEFAULT_OUTPUT --- src/runes/runestone.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/runes/runestone.rs b/src/runes/runestone.rs index fa949cd85e..564a0452cc 100644 --- a/src/runes/runestone.rs +++ b/src/runes/runestone.rs @@ -6,7 +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: u128 = 12; +const TAG_DEFAULT_OUTPUT: u128 = 12; const TAG_DIVISIBILITY: u128 = 1; const TAG_SPACERS: u128 = 3; @@ -89,7 +89,7 @@ impl Runestone { let spacers = fields.remove(&TAG_SPACERS); let symbol = fields.remove(&TAG_SYMBOL); let term = fields.remove(&TAG_TERM); - let default = fields.remove(&TAG_DEFAULT); + let default = fields.remove(&TAG_DEFAULT_OUTPUT); let etch = flags & FLAG_ETCH != 0; let unrecognized_flags = flags & !FLAG_ETCH != 0; @@ -168,7 +168,7 @@ impl Runestone { } if let Some(default) = self.default { - varint::encode_to_vec(TAG_DEFAULT, &mut payload); + varint::encode_to_vec(TAG_DEFAULT_OUTPUT, &mut payload); varint::encode_to_vec(default.into(), &mut payload); } @@ -1461,7 +1461,7 @@ mod tests { 3, TAG_TERM, 5, - TAG_DEFAULT, + TAG_DEFAULT_OUTPUT, 11, TAG_BODY, 6, From c6def5769464fcec10dd1acfe76ae4bda9a89b56 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 25 Dec 2023 12:32:42 -0800 Subject: [PATCH 5/8] Tweak --- src/index/updater/rune_updater.rs | 2 +- src/runes/runestone.rs | 10 +++++----- src/subcommand/wallet/etch.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 2728a81876..3ce5d298cb 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -65,7 +65,7 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { let default_output = runestone.as_ref().and_then(|runestone| { runestone - .default + .default_output .and_then(|default| usize::try_from(default).ok()) }); diff --git a/src/runes/runestone.rs b/src/runes/runestone.rs index 564a0452cc..db2172e02a 100644 --- a/src/runes/runestone.rs +++ b/src/runes/runestone.rs @@ -26,7 +26,7 @@ const MAX_SPACERS: u32 = 0b00000111_11111111_11111111_11111111; pub struct Runestone { pub edicts: Vec, pub etching: Option, - pub default: Option, + pub default_output: Option, pub burn: bool, } @@ -89,7 +89,7 @@ impl Runestone { let spacers = fields.remove(&TAG_SPACERS); let symbol = fields.remove(&TAG_SYMBOL); let term = fields.remove(&TAG_TERM); - let default = fields.remove(&TAG_DEFAULT_OUTPUT); + let default_output = fields.remove(&TAG_DEFAULT_OUTPUT); let etch = flags & FLAG_ETCH != 0; let unrecognized_flags = flags & !FLAG_ETCH != 0; @@ -118,7 +118,7 @@ impl Runestone { Ok(Some(Self { burn: unrecognized_flags || fields.keys().any(|tag| tag % 2 == 0), - default: default.and_then(|default| u32::try_from(default).ok()), + default_output: default_output.and_then(|default| u32::try_from(default).ok()), edicts: body, etching, })) @@ -167,9 +167,9 @@ impl Runestone { } } - if let Some(default) = self.default { + if let Some(default_output) = self.default_output { varint::encode_to_vec(TAG_DEFAULT_OUTPUT, &mut payload); - varint::encode_to_vec(default.into(), &mut payload); + varint::encode_to_vec(default_output.into(), &mut payload); } if self.burn { diff --git a/src/subcommand/wallet/etch.rs b/src/subcommand/wallet/etch.rs index b2f77fd97e..6c02fe06ac 100644 --- a/src/subcommand/wallet/etch.rs +++ b/src/subcommand/wallet/etch.rs @@ -75,7 +75,7 @@ impl Etch { id: 0, output: 1, }], - default: None, + default_output: None, burn: false, }; From 5b58919de3a6105834eb413c9e1d089040db9dc4 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 25 Dec 2023 12:34:02 -0800 Subject: [PATCH 6/8] Tweak --- src/runes.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/runes.rs b/src/runes.rs index 3c4eeeb0c6..aa068d9138 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -892,7 +892,7 @@ mod tests { rune: Some(Rune(RUNE)), ..Default::default() }), - default: None, + default_output: None, burn: true, } .encipher(), @@ -945,7 +945,7 @@ mod tests { term: Some(1), spacers: 1, }), - default: None, + default_output: None, burn: true, } .encipher(), @@ -999,7 +999,7 @@ mod tests { }], etching: Some(Etching::default()), burn: true, - default: None, + default_output: None, } .encipher(), ), @@ -1328,7 +1328,7 @@ mod tests { outputs: 2, op_return: Some( Runestone { - default: Some(1), + default_output: Some(1), ..Default::default() } .encipher(), @@ -1417,7 +1417,7 @@ mod tests { outputs: 2, op_return: Some( Runestone { - default: Some(3), + default_output: Some(3), ..Default::default() } .encipher(), @@ -1506,7 +1506,7 @@ mod tests { outputs: 2, op_return: Some( Runestone { - default: Some(2), + default_output: Some(2), ..Default::default() } .encipher(), From e46b77ae5db9e418e0cd0345f2ccbe38f02c4af0 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 25 Dec 2023 12:34:07 -0800 Subject: [PATCH 7/8] Tweak --- src/runes/runestone.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runes/runestone.rs b/src/runes/runestone.rs index db2172e02a..e1cf9aa63e 100644 --- a/src/runes/runestone.rs +++ b/src/runes/runestone.rs @@ -872,7 +872,7 @@ mod tests { output: 3, }], etching: None, - default: None, + default_output: None, burn: false, }, ); @@ -1441,7 +1441,7 @@ mod tests { output: 7, }, ], - default: Some(11), + default_output: Some(11), burn: false, }, &[ From 4a6dd75e8e248017a211af4fb2ed8a1bf72a87fa Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 25 Dec 2023 12:35:13 -0800 Subject: [PATCH 8/8] Tweak --- tests/wallet/send.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index 69fbd0a485..cb17c44497 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -848,7 +848,7 @@ fn sending_rune_creates_transaction_with_expected_runestone() { assert_eq!( Runestone::from_transaction(&tx).unwrap(), Runestone { - default: None, + default_output: None, etching: None, edicts: vec![Edict { id: RuneId {