From 13a7cc3739da11bf76a081f2feae0b77a22ccf2f Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 13 Dec 2023 19:58:06 -0800 Subject: [PATCH] Reserve runes for sequential allocation (#2831) --- src/index.rs | 3 +- src/index/updater/rune_updater.rs | 30 +- src/runes.rs | 371 ++++++++++++--- src/runes/etching.rs | 2 +- src/runes/rune.rs | 33 ++ src/runes/runestone.rs | 757 ++++++++++++++++-------------- src/subcommand/server.rs | 10 +- src/subcommand/wallet/etch.rs | 10 +- tests/etch.rs | 21 +- 9 files changed, 801 insertions(+), 436 deletions(-) diff --git a/src/index.rs b/src/index.rs index 3394701202..736b888c35 100644 --- a/src/index.rs +++ b/src/index.rs @@ -39,7 +39,7 @@ mod updater; #[cfg(test)] pub(crate) mod testing; -const SCHEMA_VERSION: u64 = 13; +const SCHEMA_VERSION: u64 = 14; macro_rules! define_table { ($name:ident, $key:ty, $value:ty) => { @@ -91,6 +91,7 @@ pub(crate) enum Statistic { IndexSats, LostSats, OutputsTraversed, + ReservedRunes, Runes, SatRanges, UnboundInscriptions, diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index f3c61a3210..7eb5d40041 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -68,10 +68,34 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { // Determine if this runestone contains a valid issuance let mut allocation = match runestone.etching { Some(etching) => { - // If the issuance symbol is already taken, the issuance is ignored - if etching.rune < self.minimum || self.rune_to_id.get(etching.rune.0)?.is_some() { + if etching + .rune + .map(|rune| rune < self.minimum || rune.is_reserved()) + .unwrap_or_default() + || etching + .rune + .and_then(|rune| self.rune_to_id.get(rune.0).transpose()) + .transpose()? + .is_some() + { None } else { + let rune = if let Some(rune) = etching.rune { + rune + } else { + let reserved_runes = self + .statistic_to_count + .get(&Statistic::ReservedRunes.into())? + .map(|entry| entry.value()) + .unwrap_or_default(); + + self + .statistic_to_count + .insert(&Statistic::ReservedRunes.into(), reserved_runes + 1)?; + + Rune::reserved(reserved_runes.into()) + }; + let (limit, term) = match (etching.limit, etching.term) { (None, Some(term)) => (Some(runes::MAX_LIMIT), Some(term)), (limit, term) => (limit, term), @@ -96,7 +120,7 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { limit, divisibility: etching.divisibility, id: u128::from(self.height) << 16 | u128::from(index), - rune: etching.rune, + rune, symbol: etching.symbol, end: term.map(|term| term + self.height), }), diff --git a/src/runes.rs b/src/runes.rs index 2e72ba8f71..1cae1b378e 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -7,6 +7,7 @@ pub(crate) use {edict::Edict, etching::Etching, pile::Pile}; pub const MAX_DIVISIBILITY: u8 = 38; pub(crate) const CLAIM_BIT: u128 = 1 << 48; pub(crate) const MAX_LIMIT: u128 = 1 << 64; +const RESERVED: u128 = 6402364363415443603228541259936211926; mod edict; mod etching; @@ -46,7 +47,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -89,7 +90,7 @@ mod tests { op_return: Some( Runestone { etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -136,7 +137,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -190,7 +191,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(SECOND_BLOCK_LOCKED_RUNE - 1), + rune: Some(Rune(SECOND_BLOCK_LOCKED_RUNE - 1)), ..Default::default() }), ..Default::default() @@ -220,7 +221,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(SECOND_BLOCK_LOCKED_RUNE), + rune: Some(Rune(SECOND_BLOCK_LOCKED_RUNE)), ..Default::default() }), ..Default::default() @@ -253,6 +254,212 @@ mod tests { } } + #[test] + fn etching_cannot_specify_reserved_rune() { + { + let context = Context::builder().arg("--index-runes").build(); + + context.mine_blocks(1); + + 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(RESERVED)), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + context.assert_runes([], []); + } + + { + let context = Context::builder().arg("--index-runes").build(); + + context.mine_blocks(1); + + let txid = 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(RESERVED - 1)), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + context.assert_runes( + [( + id, + RuneEntry { + etching: txid, + rune: Rune(RESERVED - 1), + supply: u128::max_value(), + timestamp: 2, + ..Default::default() + }, + )], + [(OutPoint { txid, vout: 0 }, vec![(id, u128::max_value())])], + ); + } + } + + #[test] + fn reserved_runes_may_be_etched() { + 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())], + outputs: 2, + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: None, + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + let id0 = RuneId { + height: 2, + index: 1, + }; + + context.mine_blocks(1); + + context.assert_runes( + [( + id0, + RuneEntry { + etching: txid0, + rune: Rune(RESERVED), + supply: u128::max_value(), + timestamp: 2, + ..Default::default() + }, + )], + [( + OutPoint { + txid: txid0, + vout: 0, + }, + vec![(id0, u128::max_value())], + )], + ); + + context.mine_blocks(1); + + let txid1 = 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: None, + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id1 = RuneId { + height: 4, + index: 1, + }; + + context.assert_runes( + [ + ( + id0, + RuneEntry { + etching: txid0, + rune: Rune(RESERVED), + supply: u128::max_value(), + timestamp: 2, + ..Default::default() + }, + ), + ( + id1, + RuneEntry { + etching: txid1, + rune: Rune(RESERVED + 1), + supply: u128::max_value(), + timestamp: 4, + number: 1, + ..Default::default() + }, + ), + ], + [ + ( + OutPoint { + txid: txid0, + vout: 0, + }, + vec![(id0, u128::max_value())], + ), + ( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id1, u128::max_value())], + ), + ], + ); + } + #[test] fn etching_with_non_zero_divisibility_and_rune() { let context = Context::builder().arg("--index-runes").build(); @@ -270,7 +477,7 @@ mod tests { }], etching: Some(Etching { divisibility: 1, - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -326,7 +533,7 @@ mod tests { }, ], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -381,7 +588,7 @@ mod tests { }, ], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -430,7 +637,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -485,7 +692,7 @@ mod tests { }, ], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -541,7 +748,7 @@ mod tests { }, ], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -589,7 +796,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -666,7 +873,7 @@ mod tests { } #[test] - fn etched_rune_is_burned_if_an_unrecognized_even_tag_is_encountered() { + fn etched_rune_is_allocated_with_zero_supply_for_burned_runestone() { let context = Context::builder().arg("--index-runes").build(); context.mine_blocks(1); @@ -681,7 +888,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), burn: true, @@ -712,6 +919,50 @@ mod tests { ); } + #[test] + fn etched_reserved_rune_is_allocated_with_zero_supply_for_burned_runestone() { + 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::default()), + burn: true, + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + context.assert_runes( + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RESERVED), + timestamp: 2, + ..Default::default() + }, + )], + [], + ); + } + #[test] fn input_runes_are_burned_if_an_unrecognized_even_tag_is_encountered() { let context = Context::builder().arg("--index-runes").build(); @@ -728,7 +979,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -811,7 +1062,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -893,7 +1144,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -972,7 +1223,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -1054,7 +1305,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -1095,7 +1346,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -1138,7 +1389,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -1185,7 +1436,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE + 1), + rune: Some(Rune(RUNE + 1)), ..Default::default() }), ..Default::default() @@ -1301,7 +1552,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -1348,7 +1599,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE + 1), + rune: Some(Rune(RUNE + 1)), ..Default::default() }), ..Default::default() @@ -1535,7 +1786,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -1582,7 +1833,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE + 1), + rune: Some(Rune(RUNE + 1)), ..Default::default() }), ..Default::default() @@ -1717,7 +1968,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -1798,7 +2049,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -1823,7 +2074,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE + 1), + rune: Some(Rune(RUNE + 1)), ..Default::default() }), ..Default::default() @@ -1899,7 +2150,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -1998,7 +2249,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2045,7 +2296,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE + 1), + rune: Some(Rune(RUNE + 1)), ..Default::default() }), ..Default::default() @@ -2188,7 +2439,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2280,7 +2531,7 @@ mod tests { output: 1, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2330,7 +2581,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2386,7 +2637,7 @@ mod tests { }, ], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2435,7 +2686,7 @@ mod tests { output: 5, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2508,7 +2759,7 @@ mod tests { }, ], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2581,7 +2832,7 @@ mod tests { }, ], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2647,7 +2898,7 @@ mod tests { output: 5, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2708,7 +2959,7 @@ mod tests { }, ], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2771,7 +3022,7 @@ mod tests { }, ], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2827,7 +3078,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2929,7 +3180,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -3038,7 +3289,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -3147,7 +3398,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -3249,7 +3500,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -3358,7 +3609,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -3481,7 +3732,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), symbol: Some('$'), ..Default::default() }), @@ -3531,7 +3782,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -3579,7 +3830,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -3674,7 +3925,7 @@ mod tests { op_return: Some( Runestone { etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), limit: Some(1000), ..Default::default() }), @@ -3805,7 +4056,7 @@ mod tests { op_return: Some( Runestone { etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), limit: Some(1000), term: Some(2), ..Default::default() @@ -3936,7 +4187,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), limit: Some(1000), term: Some(0), ..Default::default() @@ -4016,7 +4267,7 @@ mod tests { op_return: Some( Runestone { etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), limit: Some(1000), ..Default::default() }), @@ -4109,7 +4360,7 @@ mod tests { op_return: Some( Runestone { etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), limit: Some(1000), ..Default::default() }), @@ -4159,7 +4410,7 @@ mod tests { op_return: Some( Runestone { etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), limit: Some(MAX_LIMIT + 1), ..Default::default() }), @@ -4233,7 +4484,7 @@ mod tests { op_return: Some( Runestone { etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), term: Some(1), ..Default::default() }), @@ -4278,7 +4529,7 @@ mod tests { op_return: Some( Runestone { etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), limit: Some(1000), ..Default::default() }), @@ -4387,7 +4638,7 @@ mod tests { op_return: Some( Runestone { etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), limit: Some(1000), ..Default::default() }), diff --git a/src/runes/etching.rs b/src/runes/etching.rs index e24cacd2e9..c110e26969 100644 --- a/src/runes/etching.rs +++ b/src/runes/etching.rs @@ -4,7 +4,7 @@ use super::*; pub struct Etching { pub(crate) divisibility: u8, pub(crate) limit: Option, - pub(crate) rune: Rune, + pub(crate) rune: Option, pub(crate) symbol: Option, pub(crate) term: Option, } diff --git a/src/runes/rune.rs b/src/runes/rune.rs index e2c14db1dd..8dd061bff0 100644 --- a/src/runes/rune.rs +++ b/src/runes/rune.rs @@ -62,6 +62,14 @@ impl Rune { Rune(start - ((start - end) * remainder / u128::from(INTERVAL))) } + + pub(crate) fn is_reserved(self) -> bool { + self.0 >= RESERVED + } + + pub(crate) fn reserved(n: u128) -> Self { + Rune(RESERVED.checked_add(n).unwrap()) + } } impl Serialize for Rune { @@ -303,6 +311,31 @@ mod tests { assert_eq!(serde_json::from_str::(json).unwrap(), rune); } + #[test] + fn reserved() { + assert_eq!( + RESERVED, + "AAAAAAAAAAAAAAAAAAAAAAAAAAA".parse::().unwrap().0, + ); + + assert_eq!(Rune::reserved(0), Rune(RESERVED)); + assert_eq!(Rune::reserved(1), Rune(RESERVED + 1)); + } + + #[test] + fn is_reserved() { + #[track_caller] + fn case(rune: &str, reserved: bool) { + assert_eq!(rune.parse::().unwrap().is_reserved(), reserved); + } + + case("A", false); + case("ZZZZZZZZZZZZZZZZZZZZZZZZZZ", false); + case("AAAAAAAAAAAAAAAAAAAAAAAAAAA", true); + case("AAAAAAAAAAAAAAAAAAAAAAAAAAB", true); + case("BCGDENLQRQWDSLRUGSNLBTMFIJAV", true); + } + #[test] fn steps() { for i in 0.. { diff --git a/src/runes/runestone.rs b/src/runes/runestone.rs index 46c3d9717d..2bde353755 100644 --- a/src/runes/runestone.rs +++ b/src/runes/runestone.rs @@ -6,9 +6,15 @@ const TAG_RUNE: u128 = 2; const TAG_SYMBOL: u128 = 3; const TAG_LIMIT: u128 = 4; const TAG_TERM: u128 = 6; +const TAG_FLAGS: u128 = 8; + +const FLAG_ETCH: u128 = 0b000_0001; + +#[allow(unused)] +const TAG_BURN: u128 = 254; #[allow(unused)] -const TAG_BURN: u128 = 256; +const TAG_NOP: u128 = 255; #[derive(Default, Serialize, Debug, PartialEq)] pub struct Runestone { @@ -68,29 +74,37 @@ impl Runestone { let Message { mut fields, body } = Message::from_integers(&integers); - let etching = fields.remove(&TAG_RUNE).map(|rune| Etching { - divisibility: fields - .remove(&TAG_DIVISIBILITY) - .and_then(|divisibility| u8::try_from(divisibility).ok()) - .and_then(|divisibility| (divisibility <= MAX_DIVISIBILITY).then_some(divisibility)) - .unwrap_or_default(), - limit: fields - .remove(&TAG_LIMIT) - .and_then(|limit| (limit <= MAX_LIMIT).then_some(limit)), - rune: Rune(rune), - symbol: fields - .remove(&TAG_SYMBOL) - .and_then(|symbol| u32::try_from(symbol).ok()) - .and_then(char::from_u32), - term: fields - .remove(&TAG_TERM) - .and_then(|term| u32::try_from(term).ok()), - }); + let flags = fields.remove(&TAG_FLAGS).unwrap_or_default(); + let etch = flags & FLAG_ETCH != 0; + let unrecognized_flags = flags & !FLAG_ETCH != 0; + + let divisibility = fields.remove(&TAG_DIVISIBILITY); + let limit = fields.remove(&TAG_LIMIT); + let rune = fields.remove(&TAG_RUNE); + let symbol = fields.remove(&TAG_SYMBOL); + let term = fields.remove(&TAG_TERM); + + let etching = if etch { + Some(Etching { + divisibility: divisibility + .and_then(|divisibility| u8::try_from(divisibility).ok()) + .and_then(|divisibility| (divisibility <= MAX_DIVISIBILITY).then_some(divisibility)) + .unwrap_or_default(), + limit: limit.and_then(|limit| (limit <= MAX_LIMIT).then_some(limit)), + rune: rune.map(Rune), + symbol: symbol + .and_then(|symbol| u32::try_from(symbol).ok()) + .and_then(char::from_u32), + term: term.and_then(|term| u32::try_from(term).ok()), + }) + } else { + None + }; Ok(Some(Self { edicts: body, etching, - burn: fields.keys().any(|tag| tag % 2 == 0), + burn: unrecognized_flags || fields.keys().any(|tag| tag % 2 == 0), })) } @@ -98,8 +112,13 @@ impl Runestone { let mut payload = Vec::new(); if let Some(etching) = self.etching { - varint::encode_to_vec(TAG_RUNE, &mut payload); - varint::encode_to_vec(etching.rune.0, &mut payload); + varint::encode_to_vec(TAG_FLAGS, &mut payload); + varint::encode_to_vec(FLAG_ETCH, &mut payload); + + if let Some(rune) = etching.rune { + varint::encode_to_vec(TAG_RUNE, &mut payload); + varint::encode_to_vec(rune.0, &mut payload); + } if etching.divisibility != 0 { varint::encode_to_vec(TAG_DIVISIBILITY, &mut payload); @@ -201,6 +220,38 @@ mod tests { bitcoin::{locktime, script::PushBytes, ScriptBuf, TxOut}, }; + fn decipher(integers: &[u128]) -> Runestone { + let payload = payload(integers); + + let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice(b"RUNE_TEST") + .push_slice(payload) + .into_script(), + value: 0, + }], + lock_time: locktime::absolute::LockTime::ZERO, + version: 0, + }) + .unwrap() + .unwrap() + } + + fn payload(integers: &[u128]) -> Vec { + let mut payload = Vec::new(); + + for integer in integers { + payload.extend(varint::encode(*integer)); + } + + payload + } + #[test] fn from_transaction_returns_none_if_decipher_returns_error() { assert_eq!( @@ -387,16 +438,6 @@ mod tests { ); } - fn payload(integers: &[u128]) -> Vec { - let mut payload = Vec::new(); - - for integer in integers { - payload.extend(varint::encode(*integer)); - } - - payload - } - #[test] fn error_in_input_aborts_search_for_runestone() { let payload = payload(&[0, 1, 2, 3]); @@ -433,436 +474,439 @@ mod tests { #[test] fn deciphering_non_empty_runestone_is_successful() { - let payload = payload(&[0, 1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + decipher(&[TAG_BODY, 1, 2, 3]), + Runestone { edicts: vec![Edict { id: 1, amount: 2, output: 3, }], ..Default::default() - })) + } ); } #[test] fn decipher_etching() { - let payload = payload(&[2, 4, 0, 1, 2, 3]); + assert_eq!( + decipher(&[TAG_FLAGS, FLAG_ETCH, TAG_BODY, 1, 2, 3]), + Runestone { + edicts: vec![Edict { + id: 1, + amount: 2, + output: 3, + }], + etching: Some(Etching::default()), + ..Default::default() + } + ); + } - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + #[test] + fn decipher_etching_with_rune() { + assert_eq!( + decipher(&[TAG_FLAGS, FLAG_ETCH, TAG_RUNE, 4, TAG_BODY, 1, 2, 3]), + Runestone { + edicts: vec![Edict { + id: 1, + amount: 2, + output: 3, + }], + etching: Some(Etching { + rune: Some(Rune(4)), + ..Default::default() + }), + ..Default::default() + }, + ); + } + #[test] + fn decipher_etching_with_term() { assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 + decipher(&[TAG_FLAGS, FLAG_ETCH, TAG_TERM, 4, TAG_BODY, 1, 2, 3]), + Runestone { + edicts: vec![Edict { + id: 1, + amount: 2, + output: 3, }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + etching: Some(Etching { + term: Some(4), + ..Default::default() + }), + ..Default::default() + }, + ); + } + + #[test] + fn decipher_etching_with_limit() { + assert_eq!( + decipher(&[TAG_FLAGS, FLAG_ETCH, TAG_LIMIT, 4, TAG_BODY, 1, 2, 3]), + Runestone { edicts: vec![Edict { id: 1, amount: 2, output: 3, }], etching: Some(Etching { - rune: Rune(4), + limit: Some(4), ..Default::default() }), ..Default::default() - })) + }, ); } #[test] fn duplicate_tags_are_ignored() { - let payload = payload(&[2, 4, 2, 5, 0, 1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + decipher(&[TAG_FLAGS, FLAG_ETCH, TAG_RUNE, 4, TAG_RUNE, 5, TAG_BODY, 1, 2, 3,]), + Runestone { edicts: vec![Edict { id: 1, amount: 2, output: 3, }], etching: Some(Etching { - rune: Rune(4), + rune: Some(Rune(4)), ..Default::default() }), ..Default::default() - })) + } ); } #[test] fn unrecognized_odd_tag_is_ignored() { - let payload = payload(&[127, 100, 0, 1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + assert_eq!( + decipher(&[TAG_NOP, 100, TAG_BODY, 1, 2, 3]), + Runestone { + edicts: vec![Edict { + id: 1, + amount: 2, + output: 3, + }], + ..Default::default() + }, + ); + } + #[test] + fn unrecognized_even_tag_is_burn() { assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 + decipher(&[TAG_BURN, 0, TAG_BODY, 1, 2, 3]), + Runestone { + edicts: vec![Edict { + id: 1, + amount: 2, + output: 3, }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + burn: true, + ..Default::default() + }, + ); + } + + #[test] + fn unrecognized_flag_is_burn() { + assert_eq!( + decipher(&[TAG_FLAGS, 1 << 1, TAG_BODY, 1, 2, 3]), + Runestone { edicts: vec![Edict { id: 1, amount: 2, output: 3, }], + burn: true, ..Default::default() - })) + }, ); } #[test] fn tag_with_no_value_is_ignored() { - let payload = payload(&[2, 4, 2]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + assert_eq!( + decipher(&[TAG_FLAGS, 1, TAG_FLAGS]), + Runestone { + etching: Some(Etching::default()), + ..Default::default() + }, + ); + } + #[test] + fn additional_integers_in_body_are_ignored() { assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 + decipher(&[TAG_FLAGS, FLAG_ETCH, TAG_RUNE, 4, TAG_BODY, 1, 2, 3, 4, 5]), + Runestone { + edicts: vec![Edict { + id: 1, + amount: 2, + output: 3, }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { etching: Some(Etching { - rune: Rune(4), + rune: Some(Rune(4)), ..Default::default() }), ..Default::default() - })) + }, ); } #[test] - fn additional_integers_in_body_are_ignored() { - let payload = payload(&[2, 4, 0, 1, 2, 3, 4, 5]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - + fn decipher_etching_with_divisibility() { assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + decipher(&[ + TAG_FLAGS, + FLAG_ETCH, + TAG_RUNE, + 4, + TAG_DIVISIBILITY, + 5, + TAG_BODY, + 1, + 2, + 3, + ]), + Runestone { edicts: vec![Edict { id: 1, amount: 2, output: 3, }], etching: Some(Etching { - rune: Rune(4), + rune: Some(Rune(4)), + divisibility: 5, ..Default::default() }), ..Default::default() - })) + }, ); } #[test] - fn decipher_etching_with_divisibility() { - let payload = payload(&[2, 4, 1, 5, 0, 1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - + fn divisibility_above_max_is_ignored() { assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 + decipher(&[ + TAG_FLAGS, + FLAG_ETCH, + TAG_RUNE, + 4, + TAG_DIVISIBILITY, + (MAX_DIVISIBILITY + 1).into(), + TAG_BODY, + 1, + 2, + 3, + ]), + Runestone { + edicts: vec![Edict { + id: 1, + amount: 2, + output: 3, }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + etching: Some(Etching { + rune: Some(Rune(4)), + ..Default::default() + }), + ..Default::default() + }, + ); + } + + #[test] + fn symbol_above_max_is_ignored() { + assert_eq!( + decipher(&[ + TAG_FLAGS, + FLAG_ETCH, + TAG_SYMBOL, + u128::from(u32::from(char::MAX) + 1), + TAG_BODY, + 1, + 2, + 3, + ]), + Runestone { edicts: vec![Edict { id: 1, amount: 2, output: 3, }], - etching: Some(Etching { - rune: Rune(4), - divisibility: 5, - ..Default::default() - }), + etching: Some(Etching::default()), ..Default::default() - })) + }, ); } #[test] - fn divisibility_above_max_is_ignored() { - let payload = payload(&[2, 4, 1, (MAX_DIVISIBILITY + 1).into(), 0, 1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - + fn decipher_etching_with_symbol() { assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + decipher(&[ + TAG_FLAGS, + FLAG_ETCH, + TAG_RUNE, + 4, + TAG_SYMBOL, + 'a'.into(), + TAG_BODY, + 1, + 2, + 3, + ]), + Runestone { edicts: vec![Edict { id: 1, amount: 2, output: 3, }], etching: Some(Etching { - rune: Rune(4), + rune: Some(Rune(4)), + symbol: Some('a'), ..Default::default() }), ..Default::default() - })) + }, ); } #[test] - fn symbol_above_max_is_ignored() { - let payload = payload(&[2, 4, 3, u128::from(u32::from(char::MAX) + 1), 0, 1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - + fn decipher_etching_with_all_etching_tags() { assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + decipher(&[ + TAG_FLAGS, + FLAG_ETCH, + TAG_RUNE, + 4, + TAG_DIVISIBILITY, + 1, + TAG_SYMBOL, + 'a'.into(), + TAG_TERM, + 2, + TAG_LIMIT, + 3, + TAG_BODY, + 1, + 2, + 3, + ]), + Runestone { edicts: vec![Edict { id: 1, amount: 2, output: 3, }], etching: Some(Etching { - rune: Rune(4), - ..Default::default() + rune: Some(Rune(4)), + divisibility: 1, + symbol: Some('a'), + term: Some(2), + limit: Some(3), }), ..Default::default() - })) + }, ); } #[test] - fn decipher_etching_with_symbol() { - let payload = payload(&[2, 4, 3, 'a'.into(), 0, 1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - + fn recognized_even_etching_fields_in_non_etching_are_ignored() { assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + decipher(&[ + TAG_RUNE, + 4, + TAG_DIVISIBILITY, + 1, + TAG_SYMBOL, + 'a'.into(), + TAG_TERM, + 2, + TAG_LIMIT, + 3, + TAG_BODY, + 1, + 2, + 3, + ]), + Runestone { edicts: vec![Edict { id: 1, amount: 2, output: 3, }], - etching: Some(Etching { - rune: Rune(4), - symbol: Some('a'), - ..Default::default() - }), - ..Default::default() - })) + etching: None, + burn: false, + }, ); } #[test] fn decipher_etching_with_divisibility_and_symbol() { - let payload = payload(&[2, 4, 1, 1, 3, 'a'.into(), 0, 1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + decipher(&[ + TAG_FLAGS, + FLAG_ETCH, + TAG_RUNE, + 4, + TAG_DIVISIBILITY, + 1, + TAG_SYMBOL, + 'a'.into(), + TAG_BODY, + 1, + 2, + 3, + ]), + Runestone { edicts: vec![Edict { id: 1, amount: 2, output: 3, }], etching: Some(Etching { - rune: Rune(4), + rune: Some(Rune(4)), divisibility: 1, symbol: Some('a'), ..Default::default() }), ..Default::default() - })) + }, ); } #[test] fn tag_values_are_not_parsed_as_tags() { - let payload = payload(&[2, 4, 1, 0, 0, 1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + decipher(&[ + TAG_FLAGS, + FLAG_ETCH, + TAG_DIVISIBILITY, + TAG_BODY, + TAG_BODY, + 1, + 2, + 3, + ]), + Runestone { edicts: vec![Edict { id: 1, amount: 2, output: 3, }], - etching: Some(Etching { - rune: Rune(4), - ..Default::default() - }), + etching: Some(Etching::default()), ..Default::default() - })) + }, ); } #[test] fn runestone_may_contain_multiple_edicts() { - let payload = payload(&[0, 1, 2, 3, 3, 5, 6]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + decipher(&[TAG_BODY, 1, 2, 3, 3, 5, 6]), + Runestone { edicts: vec![ Edict { id: 1, @@ -876,31 +920,15 @@ mod tests { }, ], ..Default::default() - })) + }, ); } #[test] fn id_deltas_saturate_to_max() { - let payload = payload(&[0, 1, 2, 3, u128::max_value(), 5, 6]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { + decipher(&[TAG_BODY, 1, 2, 3, u128::max_value(), 5, 6]), + Runestone { edicts: vec![ Edict { id: 1, @@ -914,7 +942,7 @@ mod tests { }, ], ..Default::default() - })) + }, ); } @@ -927,11 +955,16 @@ mod tests { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) .push_slice(b"RUNE_TEST") - .push_slice::<&PushBytes>(varint::encode(2).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>(varint::encode(4).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(TAG_FLAGS).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(FLAG_ETCH).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>( + varint::encode(TAG_DIVISIBILITY) + .as_slice() + .try_into() + .unwrap() + ) .push_slice::<&PushBytes>(varint::encode(5).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>(varint::encode(0).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(TAG_BODY).as_slice().try_into().unwrap()) .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) .push_slice::<&PushBytes>(varint::encode(2).as_slice().try_into().unwrap()) .push_slice::<&PushBytes>(varint::encode(3).as_slice().try_into().unwrap()) @@ -948,7 +981,6 @@ mod tests { output: 3, }], etching: Some(Etching { - rune: Rune(4), divisibility: 5, ..Default::default() }), @@ -1057,40 +1089,40 @@ mod tests { case( Vec::new(), Some(Etching { - rune: Rune(0), + rune: Some(Rune(0)), ..Default::default() }), - 4, + 6, ); case( Vec::new(), Some(Etching { divisibility: MAX_DIVISIBILITY, - rune: Rune(0), + rune: Some(Rune(0)), ..Default::default() }), - 6, + 8, ); case( Vec::new(), Some(Etching { divisibility: MAX_DIVISIBILITY, - rune: Rune(0), + rune: Some(Rune(0)), symbol: Some('$'), ..Default::default() }), - 8, + 10, ); case( Vec::new(), Some(Etching { - rune: Rune(u128::max_value()), + rune: Some(Rune(u128::max_value())), ..Default::default() }), - 22, + 24, ); case( @@ -1105,10 +1137,10 @@ mod tests { }], Some(Etching { divisibility: MAX_DIVISIBILITY, - rune: Rune(u128::max_value()), + rune: Some(Rune(u128::max_value())), ..Default::default() }), - 28, + 30, ); case( @@ -1123,10 +1155,10 @@ mod tests { }], Some(Etching { divisibility: MAX_DIVISIBILITY, - rune: Rune(u128::max_value()), + rune: Some(Rune(u128::max_value())), ..Default::default() }), - 46, + 48, ); case( @@ -1297,31 +1329,17 @@ mod tests { #[test] fn etching_with_term_greater_than_maximum_is_ignored() { - let payload = payload(&[2, 4, 6, u128::from(u64::max_value()) + 1]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { - etching: Some(Etching { - rune: Rune(4), - ..Default::default() - }), + decipher(&[ + TAG_FLAGS, + FLAG_ETCH, + TAG_TERM, + u128::from(u64::max_value()) + 1, + ]), + Runestone { + etching: Some(Etching::default()), ..Default::default() - })) + }, ); } @@ -1368,7 +1386,7 @@ mod tests { divisibility: 1, limit: Some(2), symbol: Some('@'), - rune: Rune(3), + rune: Some(Rune(3)), term: Some(4), }), edicts: vec![ @@ -1386,6 +1404,8 @@ mod tests { burn: false, }, &[ + TAG_FLAGS, + FLAG_ETCH, TAG_RUNE, 3, TAG_DIVISIBILITY, @@ -1412,13 +1432,28 @@ mod tests { divisibility: 0, limit: None, symbol: None, - rune: Rune(3), + rune: Some(Rune(3)), + term: None, + }), + burn: false, + ..Default::default() + }, + &[TAG_FLAGS, FLAG_ETCH, TAG_RUNE, 3], + ); + + case( + Runestone { + etching: Some(Etching { + divisibility: 0, + limit: None, + symbol: None, + rune: None, term: None, }), burn: false, ..Default::default() }, - &[TAG_RUNE, 3], + &[TAG_FLAGS, FLAG_ETCH], ); case( diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 0bbf4af678..710bfd28b2 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -2079,7 +2079,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune, + rune: Some(rune), ..Default::default() }), ..Default::default() @@ -2125,7 +2125,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2192,7 +2192,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune, + rune: Some(rune), symbol: Some('$'), ..Default::default() }), @@ -2300,7 +2300,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Rune(RUNE), + rune: Some(Rune(RUNE)), ..Default::default() }), ..Default::default() @@ -2367,7 +2367,7 @@ mod tests { }], etching: Some(Etching { divisibility: 1, - rune, + rune: Some(rune), ..Default::default() }), ..Default::default() diff --git a/src/subcommand/wallet/etch.rs b/src/subcommand/wallet/etch.rs index f9398c140d..53df76617c 100644 --- a/src/subcommand/wallet/etch.rs +++ b/src/subcommand/wallet/etch.rs @@ -37,7 +37,7 @@ impl Etch { ensure!( index.rune(self.rune)?.is_none(), "rune `{}` has already been etched", - self.rune + self.rune, ); let minimum_at_height = @@ -46,12 +46,14 @@ impl Etch { ensure!( self.rune >= minimum_at_height, "rune is less than minimum for next block: {} < {minimum_at_height}", - self.rune + self.rune, ); + ensure!(!self.rune.is_reserved(), "rune `{}` is reserved", self.rune); + ensure!( self.divisibility <= crate::runes::MAX_DIVISIBILITY, - " must be equal to or less than 38" + " must be equal to or less than 38" ); let destination = get_change_address(&client, options.chain())?; @@ -59,7 +61,7 @@ impl Etch { let runestone = Runestone { etching: Some(Etching { divisibility: self.divisibility, - rune: self.rune, + rune: Some(self.rune), limit: None, symbol: Some(self.symbol), term: None, diff --git a/tests/etch.rs b/tests/etch.rs index 1e9f92962a..dfd996bd2d 100644 --- a/tests/etch.rs +++ b/tests/etch.rs @@ -35,7 +35,7 @@ fn divisibility_over_max_is_an_error() { Rune(RUNE), )) .rpc_server(&rpc_server) - .expected_stderr("error: must be equal to or less than 38\n") + .expected_stderr("error: must be equal to or less than 38\n") .expected_exit_code(1) .run_and_extract_stdout(); } @@ -82,6 +82,25 @@ fn rune_below_minimum_is_an_error() { .run_and_extract_stdout(); } +#[test] +fn reserved_rune_is_an_error() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + rpc_server.mine_blocks(1); + + CommandBuilder::new( + "--index-runes --regtest wallet etch --rune AAAAAAAAAAAAAAAAAAAAAAAAAAA --divisibility 0 --fee-rate 1 --supply 1000 --symbol ยข" + ) + .rpc_server(&rpc_server) + .expected_stderr("error: rune `AAAAAAAAAAAAAAAAAAAAAAAAAAA` is reserved\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + #[test] fn trying_to_etch_an_existing_rune_is_an_error() { let rpc_server = test_bitcoincore_rpc::builder()