From 7c9a97b0f26b5a949004b1c70c0afd0de001137d Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 29 Mar 2024 19:09:32 -0700 Subject: [PATCH 01/15] Test that pushnum opcodes in runestone produce cenotaph --- crates/ordinals/src/runestone.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/ordinals/src/runestone.rs b/crates/ordinals/src/runestone.rs index aa0271574b..4bd6c7643b 100644 --- a/crates/ordinals/src/runestone.rs +++ b/crates/ordinals/src/runestone.rs @@ -499,6 +499,28 @@ mod tests { ); } + #[test] + fn pushnum_opcodes_in_runestone_produce_cenotaph() { + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .push_opcode(opcodes::all::OP_PUSHNUM_13) + .into_script(), + value: 0, + },], + lock_time: LockTime::ZERO, + version: 2, + }) + .unwrap() + .unwrap(), + Cenotaph::Opcode.into(), + ); + } + #[test] fn deciphering_empty_runestone_is_successful() { assert_eq!( From 8092c6caf0e41c64174b7647c0f0621b0d0e8826 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 29 Mar 2024 20:40:19 -0700 Subject: [PATCH 02/15] Dont conflate flaws and runestones --- crates/ordinals/src/artifact.rs | 7 + crates/ordinals/src/cenotaph.rs | 68 +--- crates/ordinals/src/flaw.rs | 57 +++ crates/ordinals/src/lib.rs | 10 +- crates/ordinals/src/runestone.rs | 460 ++++++++++++----------- crates/ordinals/src/runestone/message.rs | 14 +- src/index/updater/rune_updater.rs | 4 +- 7 files changed, 325 insertions(+), 295 deletions(-) create mode 100644 crates/ordinals/src/artifact.rs create mode 100644 crates/ordinals/src/flaw.rs diff --git a/crates/ordinals/src/artifact.rs b/crates/ordinals/src/artifact.rs new file mode 100644 index 0000000000..0bb61654d0 --- /dev/null +++ b/crates/ordinals/src/artifact.rs @@ -0,0 +1,7 @@ +use super::*; + +#[derive(Debug, PartialEq)] +pub enum Artifact { + Cenotaph(Cenotaph), + Runestone(Runestone), +} diff --git a/crates/ordinals/src/cenotaph.rs b/crates/ordinals/src/cenotaph.rs index a4b0b7e69e..d388208fb1 100644 --- a/crates/ordinals/src/cenotaph.rs +++ b/crates/ordinals/src/cenotaph.rs @@ -1,66 +1,8 @@ use super::*; -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum Cenotaph { - EdictOutput, - EdictRuneId, - InvalidScript, - Opcode, - SupplyOverflow, - TrailingIntegers, - TruncatedField, - UnrecognizedEvenTag, - UnrecognizedFlag, - Varint, -} - -impl Cenotaph { - pub const ALL: [Self; 10] = [ - Self::EdictOutput, - Self::EdictRuneId, - Self::InvalidScript, - Self::Opcode, - Self::SupplyOverflow, - Self::TrailingIntegers, - Self::TruncatedField, - Self::UnrecognizedEvenTag, - Self::UnrecognizedFlag, - Self::Varint, - ]; - - pub fn flag(self) -> u32 { - 1 << (self as u32) - } -} - -impl Display for Cenotaph { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - Self::EdictOutput => write!(f, "edict output greater than transaction output count"), - Self::EdictRuneId => write!(f, "invalid rune ID in edict"), - Self::InvalidScript => write!(f, "invalid script in OP_RETURN"), - Self::Opcode => write!(f, "non-pushdata opcode in OP_RETURN"), - Self::SupplyOverflow => write!(f, "supply overflows u128"), - Self::TrailingIntegers => write!(f, "trailing integers in body"), - Self::TruncatedField => write!(f, "field with missing value"), - Self::UnrecognizedEvenTag => write!(f, "unrecognized even tag"), - Self::UnrecognizedFlag => write!(f, "unrecognized field"), - Self::Varint => write!(f, "invalid varint"), - } - } -} - -impl From for Runestone { - fn from(cenotaph: Cenotaph) -> Self { - Self { - cenotaph: cenotaph.flag(), - ..default() - } - } -} - -impl From for u32 { - fn from(cenotaph: Cenotaph) -> Self { - cenotaph.flag() - } +#[derive(Debug, PartialEq, Default)] +pub struct Cenotaph { + pub flaws: u32, + pub mint: Option, + pub etching: Option, } diff --git a/crates/ordinals/src/flaw.rs b/crates/ordinals/src/flaw.rs new file mode 100644 index 0000000000..69a2be27f8 --- /dev/null +++ b/crates/ordinals/src/flaw.rs @@ -0,0 +1,57 @@ +use super::*; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Flaw { + EdictOutput, + EdictRuneId, + InvalidScript, + Opcode, + SupplyOverflow, + TrailingIntegers, + TruncatedField, + UnrecognizedEvenTag, + UnrecognizedFlag, + Varint, +} + +impl Flaw { + pub const ALL: [Self; 10] = [ + Self::EdictOutput, + Self::EdictRuneId, + Self::InvalidScript, + Self::Opcode, + Self::SupplyOverflow, + Self::TrailingIntegers, + Self::TruncatedField, + Self::UnrecognizedEvenTag, + Self::UnrecognizedFlag, + Self::Varint, + ]; + + pub fn flag(self) -> u32 { + 1 << (self as u32) + } +} + +impl Display for Flaw { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::EdictOutput => write!(f, "edict output greater than transaction output count"), + Self::EdictRuneId => write!(f, "invalid rune ID in edict"), + Self::InvalidScript => write!(f, "invalid script in OP_RETURN"), + Self::Opcode => write!(f, "non-pushdata opcode in OP_RETURN"), + Self::SupplyOverflow => write!(f, "supply overflows u128"), + Self::TrailingIntegers => write!(f, "trailing integers in body"), + Self::TruncatedField => write!(f, "field with missing value"), + Self::UnrecognizedEvenTag => write!(f, "unrecognized even tag"), + Self::UnrecognizedFlag => write!(f, "unrecognized field"), + Self::Varint => write!(f, "invalid varint"), + } + } +} + +impl From for u32 { + fn from(cenotaph: Flaw) -> Self { + cenotaph.flag() + } +} diff --git a/crates/ordinals/src/lib.rs b/crates/ordinals/src/lib.rs index afd2aa8f1d..d37564eebb 100644 --- a/crates/ordinals/src/lib.rs +++ b/crates/ordinals/src/lib.rs @@ -26,10 +26,10 @@ use { }; pub use { - cenotaph::Cenotaph, charm::Charm, decimal_sat::DecimalSat, degree::Degree, edict::Edict, - epoch::Epoch, etching::Etching, height::Height, pile::Pile, rarity::Rarity, rune::Rune, - rune_id::RuneId, runestone::Runestone, sat::Sat, sat_point::SatPoint, spaced_rune::SpacedRune, - terms::Terms, + artifact::Artifact, cenotaph::Cenotaph, charm::Charm, decimal_sat::DecimalSat, degree::Degree, + edict::Edict, epoch::Epoch, etching::Etching, flaw::Flaw, height::Height, pile::Pile, + rarity::Rarity, rune::Rune, rune_id::RuneId, runestone::Runestone, sat::Sat, sat_point::SatPoint, + spaced_rune::SpacedRune, terms::Terms, }; pub const CYCLE_EPOCHS: u32 = 6; @@ -38,6 +38,7 @@ fn default() -> T { Default::default() } +mod artifact; mod cenotaph; mod charm; mod decimal_sat; @@ -45,6 +46,7 @@ mod degree; mod edict; mod epoch; mod etching; +mod flaw; mod height; mod pile; mod rarity; diff --git a/crates/ordinals/src/runestone.rs b/crates/ordinals/src/runestone.rs index 4bd6c7643b..089e88313b 100644 --- a/crates/ordinals/src/runestone.rs +++ b/crates/ordinals/src/runestone.rs @@ -4,9 +4,12 @@ mod flag; mod message; mod tag; +// todo: +// - exhume? +// - avoid flatten, don't return script error + #[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Runestone { - pub cenotaph: u32, pub edicts: Vec, pub etching: Option, pub mint: Option, @@ -16,41 +19,38 @@ pub struct Runestone { #[derive(Debug, PartialEq)] enum Payload { Valid(Vec), - Invalid(Cenotaph), + Invalid(Flaw), } impl Runestone { pub const MAGIC_NUMBER: opcodes::All = opcodes::all::OP_PUSHNUM_13; pub const COMMIT_INTERVAL: u16 = 6; - pub fn from_transaction(transaction: &Transaction) -> Option { + pub fn from_transaction(transaction: &Transaction) -> Option { Self::decipher(transaction).ok().flatten() } - pub fn is_cenotaph(&self) -> bool { - self.cenotaph != 0 - } - - pub fn cenotaph_reasons(&self) -> Vec { - Cenotaph::ALL - .into_iter() - .filter(|cenotaph| self.cenotaph & cenotaph.flag() != 0) - .collect() - } - - fn decipher(transaction: &Transaction) -> Result, script::Error> { + fn decipher(transaction: &Transaction) -> Result, script::Error> { let payload = match Runestone::payload(transaction)? { Some(Payload::Valid(payload)) => payload, - Some(Payload::Invalid(cenotaph)) => return Ok(Some(cenotaph.into())), + Some(Payload::Invalid(flaw)) => { + return Ok(Some(Artifact::Cenotaph(Cenotaph { + flaws: flaw.into(), + ..default() + }))) + } None => return Ok(None), }; let Some(integers) = Runestone::integers(&payload) else { - return Ok(Some(Cenotaph::Varint.into())); + return Ok(Some(Artifact::Cenotaph(Cenotaph { + flaws: Flaw::Varint.into(), + ..default() + }))); }; let Message { - mut cenotaph, + mut flaws, edicts, mut fields, } = Message::from_integers(transaction, &integers); @@ -133,24 +133,31 @@ impl Runestone { }); if overflow { - cenotaph |= Cenotaph::SupplyOverflow.flag(); + flaws |= Flaw::SupplyOverflow.flag(); } if flags != 0 { - cenotaph |= Cenotaph::UnrecognizedFlag.flag(); + flaws |= Flaw::UnrecognizedFlag.flag(); } if fields.keys().any(|tag| tag % 2 == 0) { - cenotaph |= Cenotaph::UnrecognizedEvenTag.flag(); + flaws |= Flaw::UnrecognizedEvenTag.flag(); + } + + if flaws != 0 { + return Ok(Some(Artifact::Cenotaph(Cenotaph { + flaws, + mint, + etching: etching.and_then(|etching| etching.rune), + }))); } - Ok(Some(Self { - cenotaph, + Ok(Some(Artifact::Runestone(Self { edicts, etching, mint, pointer, - })) + }))) } pub fn encipher(&self) -> ScriptBuf { @@ -188,10 +195,6 @@ impl Runestone { Tag::Pointer.encode_option(self.pointer, &mut payload); - if self.is_cenotaph() { - Tag::Cenotaph.encode([0], &mut payload); - } - if !self.edicts.is_empty() { varint::encode_to_vec(Tag::Body.into(), &mut payload); @@ -248,10 +251,10 @@ impl Runestone { payload.extend_from_slice(push.as_bytes()); } Ok(Instruction::Op(_)) => { - return Ok(Some(Payload::Invalid(Cenotaph::Opcode))); + return Ok(Some(Payload::Invalid(Flaw::Opcode))); } Err(_) => { - return Ok(Some(Payload::Invalid(Cenotaph::InvalidScript))); + return Ok(Some(Payload::Invalid(Flaw::InvalidScript))); } } } @@ -290,7 +293,7 @@ mod tests { RuneId { block: 1, tx } } - fn decipher(integers: &[u128]) -> Runestone { + fn decipher(integers: &[u128]) -> Artifact { let payload = payload(integers); let payload: &PushBytes = payload.as_slice().try_into().unwrap(); @@ -438,7 +441,7 @@ mod tests { lock_time: LockTime::ZERO, version: 2, }), - Ok(Some(Payload::Invalid(Cenotaph::InvalidScript))) + Ok(Some(Payload::Invalid(Flaw::InvalidScript))) ); } @@ -495,7 +498,10 @@ mod tests { }) .unwrap() .unwrap(), - Cenotaph::Opcode.into(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::Opcode.into(), + ..default() + }), ); } @@ -517,7 +523,10 @@ mod tests { }) .unwrap() .unwrap(), - Cenotaph::Opcode.into(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::Opcode.into(), + ..default() + }), ); } @@ -535,8 +544,10 @@ mod tests { }], lock_time: LockTime::ZERO, version: 2, - }), - Ok(Some(Runestone::default())) + }) + .unwrap() + .unwrap(), + Artifact::Runestone(Runestone::default()), ); } @@ -575,10 +586,10 @@ mod tests { }) .unwrap() .unwrap(), - Runestone { + Artifact::Runestone(Runestone { mint: Some(RuneId::new(1, 1).unwrap()), ..default() - }, + }), ); } @@ -586,14 +597,14 @@ mod tests { fn deciphering_non_empty_runestone_is_successful() { assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 2, 0]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, output: 0, }], ..default() - } + }), ); } @@ -609,7 +620,7 @@ mod tests { 2, 0 ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -617,7 +628,7 @@ mod tests { }], etching: Some(Etching::default()), ..default() - } + }), ); } @@ -635,7 +646,7 @@ mod tests { 2, 0 ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -646,7 +657,7 @@ mod tests { ..default() }), ..default() - }, + }), ); } @@ -664,14 +675,14 @@ mod tests { 2, 0 ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, output: 0, }], ..default() - }, + }), ); } @@ -689,7 +700,7 @@ mod tests { 2, 0 ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -703,7 +714,7 @@ mod tests { ..default() }), ..default() - }, + }), ); } @@ -721,7 +732,7 @@ mod tests { 2, 0 ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -735,7 +746,7 @@ mod tests { ..default() }), ..default() - }, + }), ); } @@ -757,7 +768,10 @@ mod tests { }) .unwrap() .unwrap(), - Cenotaph::Varint.into(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::Varint.into(), + ..default() + }), ); } @@ -777,19 +791,11 @@ mod tests { 2, 0, ]), - Runestone { - edicts: vec![Edict { - id: rune_id(1), - amount: 2, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(4)), - ..default() - }), - cenotaph: Cenotaph::UnrecognizedEvenTag.flag(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.into(), + etching: Some(Rune(4)), ..default() - } + }), ); } @@ -809,7 +815,7 @@ mod tests { 2, 0, ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -821,7 +827,7 @@ mod tests { ..default() }), ..default() - } + }) ); } @@ -829,14 +835,14 @@ mod tests { fn unrecognized_odd_tag_is_ignored() { assert_eq!( decipher(&[Tag::Nop.into(), 100, Tag::Body.into(), 1, 1, 2, 0]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, output: 0, }], ..default() - }, + }), ); } @@ -844,15 +850,10 @@ mod tests { fn runestone_with_unrecognized_even_tag_is_cenotaph() { assert_eq!( decipher(&[Tag::Cenotaph.into(), 0, Tag::Body.into(), 1, 1, 2, 0]), - Runestone { - edicts: vec![Edict { - id: rune_id(1), - amount: 2, - output: 0, - }], - cenotaph: Cenotaph::UnrecognizedEvenTag.flag(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), ..default() - }, + }), ); } @@ -868,15 +869,10 @@ mod tests { 2, 0 ]), - Runestone { - edicts: vec![Edict { - id: rune_id(1), - amount: 2, - output: 0, - }], - cenotaph: Cenotaph::UnrecognizedFlag.flag(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedFlag.flag(), ..default() - }, + }), ); } @@ -884,11 +880,10 @@ mod tests { fn runestone_with_edict_id_with_zero_block_and_nonzero_tx_is_cenotaph() { assert_eq!( decipher(&[Tag::Body.into(), 0, 1, 2, 0]), - Runestone { - edicts: Vec::new(), - cenotaph: Cenotaph::EdictRuneId.into(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictRuneId.into(), ..default() - }, + }), ); } @@ -896,28 +891,18 @@ mod tests { fn runestone_with_overflowing_edict_id_delta_is_cenotaph() { assert_eq!( decipher(&[Tag::Body.into(), 1, 0, 0, 0, u64::MAX.into(), 0, 0, 0]), - Runestone { - edicts: vec![Edict { - id: RuneId::new(1, 0).unwrap(), - amount: 0, - output: 0, - }], - cenotaph: Cenotaph::EdictRuneId.into(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictRuneId.into(), ..default() - }, + }), ); assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 0, 0, 0, u64::MAX.into(), 0, 0]), - Runestone { - edicts: vec![Edict { - id: RuneId::new(1, 1).unwrap(), - amount: 0, - output: 0, - }], - cenotaph: Cenotaph::EdictRuneId.into(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictRuneId.into(), ..default() - }, + }), ); } @@ -925,11 +910,10 @@ mod tests { fn runestone_with_output_over_max_is_cenotaph() { assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 2, 2]), - Runestone { - edicts: Vec::new(), - cenotaph: Cenotaph::EdictOutput.into(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictOutput.into(), ..default() - }, + }), ); } @@ -937,11 +921,10 @@ mod tests { fn tag_with_no_value_is_cenotaph() { assert_eq!( decipher(&[Tag::Flags.into(), 1, Tag::Flags.into()]), - Runestone { - etching: Some(Etching::default()), - cenotaph: Cenotaph::TruncatedField.flag(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::TruncatedField.flag(), ..default() - }, + }), ); } @@ -952,18 +935,20 @@ mod tests { for i in 0..4 { assert_eq!( decipher(&integers), - Runestone { - cenotaph: if i > 0 { - Cenotaph::TrailingIntegers.flag() - } else { - 0 - }, - edicts: vec![Edict { - id: rune_id(1), - amount: 2, - output: 0, - }], - ..default() + if i == 0 { + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + ..default() + }) + } else { + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::TrailingIntegers.into(), + ..default() + }) } ); @@ -987,7 +972,7 @@ mod tests { 2, 0, ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -999,7 +984,7 @@ mod tests { ..default() }), ..default() - }, + }), ); } @@ -1019,7 +1004,7 @@ mod tests { 2, 0, ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -1030,7 +1015,7 @@ mod tests { ..default() }), ..default() - }, + }), ); } @@ -1048,7 +1033,7 @@ mod tests { 2, 0, ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -1056,7 +1041,7 @@ mod tests { }], etching: Some(Etching::default()), ..default() - }, + }), ); } @@ -1076,7 +1061,7 @@ mod tests { 2, 0, ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -1088,7 +1073,7 @@ mod tests { ..default() }), ..default() - }, + }), ); } @@ -1126,7 +1111,7 @@ mod tests { 2, 0, ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -1145,10 +1130,9 @@ mod tests { symbol: Some('a'), spacers: Some(5), }), - cenotaph: 0, pointer: Some(0), mint: Some(RuneId::new(1, 1).unwrap()), - }, + }), ); } @@ -1172,14 +1156,14 @@ mod tests { 2, 0, ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, output: 0, }], ..default() - }, + }), ); } @@ -1201,7 +1185,7 @@ mod tests { 2, 0, ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -1214,7 +1198,7 @@ mod tests { ..default() }), ..default() - }, + }), ); } @@ -1232,7 +1216,7 @@ mod tests { 2, 0, ]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -1243,7 +1227,7 @@ mod tests { ..default() }), ..default() - }, + }), ); } @@ -1251,7 +1235,7 @@ mod tests { fn runestone_may_contain_multiple_edicts() { assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 2, 0, 0, 3, 5, 0]), - Runestone { + Artifact::Runestone(Runestone { edicts: vec![ Edict { id: rune_id(1), @@ -1265,7 +1249,7 @@ mod tests { }, ], ..default() - }, + }), ); } @@ -1273,15 +1257,10 @@ mod tests { fn runestones_with_invalid_rune_id_blocks_are_cenotaph() { assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 2, 0, u128::MAX, 1, 0, 0,]), - Runestone { - edicts: vec![Edict { - id: rune_id(1), - amount: 2, - output: 0, - }], - cenotaph: Cenotaph::EdictRuneId.flag(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictRuneId.flag(), ..default() - }, + }), ); } @@ -1289,15 +1268,10 @@ mod tests { fn runestones_with_invalid_rune_id_txs_are_cenotaph() { assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 2, 0, 1, u128::MAX, 0, 0,]), - Runestone { - edicts: vec![Edict { - id: rune_id(1), - amount: 2, - output: 0, - }], - cenotaph: Cenotaph::EdictRuneId.flag(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictRuneId.flag(), ..default() - }, + }), ); } @@ -1344,8 +1318,10 @@ mod tests { }], lock_time: LockTime::ZERO, version: 2, - }), - Ok(Some(Runestone { + }) + .unwrap() + .unwrap(), + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -1356,7 +1332,7 @@ mod tests { ..default() }), ..default() - })) + }), ); } @@ -1385,15 +1361,17 @@ mod tests { ], lock_time: LockTime::ZERO, version: 2, - }), - Ok(Some(Runestone { + }) + .unwrap() + .unwrap(), + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, output: 0, }], ..default() - })) + }), ); } @@ -1425,15 +1403,17 @@ mod tests { ], lock_time: LockTime::ZERO, version: 2, - }), - Ok(Some(Runestone { + }) + .unwrap() + .unwrap(), + Artifact::Runestone(Runestone { edicts: vec![Edict { id: rune_id(1), amount: 2, output: 0, }], ..default() - })) + }) ); } @@ -1683,11 +1663,10 @@ mod tests { Tag::OffsetEnd.into(), u128::from(u64::MAX) + 1, ]), - Runestone { - etching: Some(default()), - cenotaph: Cenotaph::UnrecognizedEvenTag.into(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.into(), ..default() - }, + }), ); } @@ -1724,7 +1703,7 @@ mod tests { assert_eq!( Runestone::from_transaction(&transaction).unwrap(), - runestone + Artifact::Runestone(runestone), ); } @@ -1732,7 +1711,6 @@ mod tests { case( Runestone { - cenotaph: Cenotaph::UnrecognizedEvenTag.into(), edicts: vec![ Edict { id: RuneId::new(2, 3).unwrap(), @@ -1792,8 +1770,6 @@ mod tests { 18, Tag::Pointer.into(), 0, - Tag::Cenotaph.into(), - 0, Tag::Body.into(), 2, 3, @@ -1816,7 +1792,6 @@ mod tests { rune: Some(Rune(3)), spacers: None, }), - cenotaph: 0, ..default() }, &[Tag::Flags.into(), Flag::Etching.mask(), Tag::Rune.into(), 3], @@ -1832,19 +1807,10 @@ mod tests { rune: None, spacers: None, }), - cenotaph: 0, ..default() }, &[Tag::Flags.into(), Flag::Etching.mask()], ); - - case( - Runestone { - cenotaph: Cenotaph::UnrecognizedEvenTag.into(), - ..default() - }, - &[Tag::Cenotaph.into(), 0], - ); } #[test] @@ -1883,73 +1849,109 @@ mod tests { #[test] fn edict_output_greater_than_32_max_produces_cenotaph() { assert_eq!( - decipher(&[Tag::Body.into(), 1, 1, 1, u128::from(u32::MAX) + 1]).cenotaph, - Cenotaph::EdictOutput.flag() + decipher(&[Tag::Body.into(), 1, 1, 1, u128::from(u32::MAX) + 1]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictOutput.flag(), + ..default() + }), ); } #[test] fn partial_mint_produces_cenotaph() { assert_eq!( - decipher(&[Tag::Mint.into(), 1]).cenotaph, - Cenotaph::UnrecognizedEvenTag.flag() + decipher(&[Tag::Mint.into(), 1]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), ); } #[test] fn invalid_mint_produces_cenotaph() { assert_eq!( - decipher(&[Tag::Mint.into(), 0, Tag::Mint.into(), 1]).cenotaph, - Cenotaph::UnrecognizedEvenTag.flag() + decipher(&[Tag::Mint.into(), 0, Tag::Mint.into(), 1]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), ); } #[test] fn invalid_deadline_produces_cenotaph() { assert_eq!( - decipher(&[Tag::OffsetEnd.into(), u128::MAX]).cenotaph, - Cenotaph::UnrecognizedEvenTag.flag() + decipher(&[Tag::OffsetEnd.into(), u128::MAX]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), ); } #[test] fn invalid_default_output_produces_cenotaph() { assert_eq!( - decipher(&[Tag::Pointer.into(), 1]).cenotaph, - Cenotaph::UnrecognizedEvenTag.flag() + decipher(&[Tag::Pointer.into(), 1]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), ); assert_eq!( - decipher(&[Tag::Pointer.into(), u128::MAX]).cenotaph, - Cenotaph::UnrecognizedEvenTag.flag() + decipher(&[Tag::Pointer.into(), u128::MAX]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), ); } #[test] fn invalid_divisibility_does_not_produce_cenotaph() { - assert!(!decipher(&[Tag::Divisibility.into(), u128::MAX]).is_cenotaph()); + assert_eq!( + decipher(&[Tag::Divisibility.into(), u128::MAX]), + Artifact::Runestone(default()), + ); } #[test] fn min_and_max_runes_are_not_cenotaphs() { - assert!(!decipher(&[Tag::Rune.into(), 0]).is_cenotaph()); - assert!(!decipher(&[Tag::Rune.into(), u128::MAX]).is_cenotaph()); + assert_eq!( + decipher(&[Tag::Rune.into(), 0]), + Artifact::Runestone(default()), + ); + assert_eq!( + decipher(&[Tag::Rune.into(), u128::MAX]), + Artifact::Runestone(default()), + ); } #[test] fn invalid_spacers_does_not_produce_cenotaph() { - assert!(!decipher(&[Tag::Spacers.into(), u128::MAX]).is_cenotaph()); + assert_eq!( + decipher(&[Tag::Spacers.into(), u128::MAX]), + Artifact::Runestone(default()), + ); } #[test] fn invalid_symbol_does_not_produce_cenotaph() { - assert!(!decipher(&[Tag::Symbol.into(), u128::MAX]).is_cenotaph()); + assert_eq!( + decipher(&[Tag::Symbol.into(), u128::MAX]), + Artifact::Runestone(default()), + ); } #[test] fn invalid_term_produces_cenotaph() { assert_eq!( - decipher(&[Tag::OffsetEnd.into(), u128::MAX]).cenotaph, - Cenotaph::UnrecognizedEvenTag.flag() + decipher(&[Tag::OffsetEnd.into(), u128::MAX]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), ); } @@ -1963,9 +1965,19 @@ mod tests { 1, Tag::Amount.into(), u128::MAX - ]) - .cenotaph, - 0, + ]), + Artifact::Runestone(Runestone { + etching: Some(Etching { + terms: Some(Terms { + cap: Some(1), + amount: Some(u128::MAX), + height: (None, None), + offset: (None, None), + }), + ..default() + }), + ..default() + }), ); assert_eq!( @@ -1976,9 +1988,11 @@ mod tests { 2, Tag::Amount.into(), u128::MAX - ]) - .cenotaph, - Cenotaph::SupplyOverflow.flag(), + ]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::SupplyOverflow.into(), + ..default() + }), ); assert_eq!( @@ -1989,9 +2003,11 @@ mod tests { 2, Tag::Amount.into(), u128::MAX / 2 + 1 - ]) - .cenotaph, - Cenotaph::SupplyOverflow.flag(), + ]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::SupplyOverflow.into(), + ..default() + }), ); assert_eq!( @@ -2004,9 +2020,11 @@ mod tests { 1, Tag::Amount.into(), u128::MAX - ]) - .cenotaph, - Cenotaph::SupplyOverflow.flag(), + ]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::SupplyOverflow.into(), + ..default() + }), ); } @@ -2058,8 +2076,9 @@ mod tests { } ], }) + .unwrap() .unwrap(), - Some(Runestone::default()) + Artifact::Runestone(Runestone::default()), ); } @@ -2084,11 +2103,12 @@ mod tests { value: 0, }], }) + .unwrap() .unwrap(), - Some(Runestone { - cenotaph: Cenotaph::InvalidScript.into(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::InvalidScript.into(), ..default() - }) + }), ); } } diff --git a/crates/ordinals/src/runestone/message.rs b/crates/ordinals/src/runestone/message.rs index f31b03d7eb..b833169fbc 100644 --- a/crates/ordinals/src/runestone/message.rs +++ b/crates/ordinals/src/runestone/message.rs @@ -1,7 +1,7 @@ use super::*; pub(super) struct Message { - pub(super) cenotaph: u32, + pub(super) flaws: u32, pub(super) edicts: Vec, pub(super) fields: HashMap>, } @@ -10,7 +10,7 @@ impl Message { pub(super) fn from_integers(tx: &Transaction, payload: &[u128]) -> Self { let mut edicts = Vec::new(); let mut fields = HashMap::>::new(); - let mut cenotaph = 0; + let mut flaws = 0; for i in (0..payload.len()).step_by(2) { let tag = payload[i]; @@ -19,17 +19,17 @@ impl Message { let mut id = RuneId::default(); for chunk in payload[i + 1..].chunks(4) { if chunk.len() != 4 { - cenotaph |= Cenotaph::TrailingIntegers.flag(); + flaws |= Flaw::TrailingIntegers.flag(); break; } let Some(next) = id.next(chunk[0], chunk[1]) else { - cenotaph |= Cenotaph::EdictRuneId.flag(); + flaws |= Flaw::EdictRuneId.flag(); break; }; let Some(edict) = Edict::from_integers(tx, next, chunk[2], chunk[3]) else { - cenotaph |= Cenotaph::EdictOutput.flag(); + flaws |= Flaw::EdictOutput.flag(); break; }; @@ -40,7 +40,7 @@ impl Message { } let Some(&value) = payload.get(i + 1) else { - cenotaph |= Cenotaph::TruncatedField.flag(); + flaws |= Flaw::TruncatedField.flag(); break; }; @@ -48,7 +48,7 @@ impl Message { } Self { - cenotaph, + flaws, edicts, fields, } diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index d71e016794..21c0121d97 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -32,7 +32,9 @@ pub(super) struct RuneUpdater<'a, 'tx, 'client> { impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { pub(super) fn index_runes(&mut self, tx_index: u32, tx: &Transaction, txid: Txid) -> Result<()> { - let runestone = Runestone::from_transaction(tx); + let Artifact::Runestone(runestone) = Runestone::from_transaction(tx) else { + todo!(); + }; let mut unallocated = self.unallocated(tx)?; From 3e8238126cb05f62448463216825bc95ee5c06fb Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 29 Mar 2024 20:56:39 -0700 Subject: [PATCH 03/15] Improve docs --- docs/src/runes/specification.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/runes/specification.md b/docs/src/runes/specification.md index 9639ddfac8..b247790bf2 100644 --- a/docs/src/runes/specification.md +++ b/docs/src/runes/specification.md @@ -371,13 +371,13 @@ The `Nop` field is unrecognized. Cenotaphs have the following effects: -- All runes input to a cenotaph are burned. +- All runes input to a transaction containing a cenotaph are burned. -- If the cenotaph is an etching, the etched rune has supply zero and is - unmintable. +- If the runestone that produced the cenotaph contained an etching, the etched + rune has supply zero and is unmintable. -- If the cenotaph is a mint, the mint counts against the mint cap and the - minted runes are burned. +- If the runestone that produced the cenotaph is a mint, the mint counts + against the mint cap and the minted runes are burned. Cenotaphs may be created if a runestone contains an unrecognized even tag, an unrecognized flag, an edict with an output number greater than the number of From 2655b3dfd712fb143529422bac8d5588a7ed384f Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 29 Mar 2024 21:03:24 -0700 Subject: [PATCH 04/15] Don't use flatten --- crates/ordinals/src/runestone.rs | 40 +++++++++++--------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/crates/ordinals/src/runestone.rs b/crates/ordinals/src/runestone.rs index 089e88313b..199b7da2ff 100644 --- a/crates/ordinals/src/runestone.rs +++ b/crates/ordinals/src/runestone.rs @@ -4,10 +4,6 @@ mod flag; mod message; mod tag; -// todo: -// - exhume? -// - avoid flatten, don't return script error - #[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Runestone { pub edicts: Vec, @@ -26,11 +22,7 @@ impl Runestone { pub const MAGIC_NUMBER: opcodes::All = opcodes::all::OP_PUSHNUM_13; pub const COMMIT_INTERVAL: u16 = 6; - pub fn from_transaction(transaction: &Transaction) -> Option { - Self::decipher(transaction).ok().flatten() - } - - fn decipher(transaction: &Transaction) -> Result, script::Error> { + pub fn decipher(transaction: &Transaction) -> Result, script::Error> { let payload = match Runestone::payload(transaction)? { Some(Payload::Valid(payload)) => payload, Some(Payload::Invalid(flaw)) => { @@ -236,9 +228,7 @@ impl Runestone { // followed by the protocol identifier, ignoring errors, since OP_RETURN // scripts may be invalid - if instructions.next().transpose().ok().flatten() - != Some(Instruction::Op(Runestone::MAGIC_NUMBER)) - { + if instructions.next() != Some(Ok(Instruction::Op(Runestone::MAGIC_NUMBER))) { continue; } @@ -326,19 +316,17 @@ mod tests { } #[test] - fn from_transaction_returns_none_if_decipher_returns_error() { - assert_eq!( - Runestone::from_transaction(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: ScriptBuf::from_bytes(vec![opcodes::all::OP_PUSHBYTES_4.to_u8()]), - value: 0, - }], - lock_time: LockTime::ZERO, - version: 2, - }), - None - ); + fn decipher_returns_an_error_if_first_opcode_is_malformed() { + assert!(Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: ScriptBuf::from_bytes(vec![opcodes::all::OP_PUSHBYTES_4.to_u8()]), + value: 0, + }], + lock_time: LockTime::ZERO, + version: 2, + }) + .is_err(),); } #[test] @@ -1702,7 +1690,7 @@ mod tests { }; assert_eq!( - Runestone::from_transaction(&transaction).unwrap(), + Runestone::decipher(&transaction).unwrap().unwrap(), Artifact::Runestone(runestone), ); } From f5b81c916419085275cd34182558eec4ce47e3e0 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 29 Mar 2024 22:25:03 -0700 Subject: [PATCH 05/15] Start fixing ord --- crates/ordinals/src/artifact.rs | 18 +++- crates/ordinals/src/cenotaph.rs | 2 +- crates/ordinals/src/lib.rs | 1 + src/index/entry.rs | 12 +++ src/index/updater/rune_updater.rs | 138 +++++++++++++++++------------- src/lib.rs | 4 +- src/runes.rs | 1 + src/subcommand/decode.rs | 6 +- src/subcommand/wallet/mint.rs | 4 +- src/subcommand/wallet/send.rs | 4 +- src/wallet/batch/plan.rs | 6 +- tests/decode.rs | 6 +- tests/lib.rs | 4 +- tests/wallet/send.rs | 7 +- 14 files changed, 130 insertions(+), 83 deletions(-) diff --git a/crates/ordinals/src/artifact.rs b/crates/ordinals/src/artifact.rs index 0bb61654d0..517a3e44c0 100644 --- a/crates/ordinals/src/artifact.rs +++ b/crates/ordinals/src/artifact.rs @@ -1,7 +1,23 @@ use super::*; -#[derive(Debug, PartialEq)] +#[derive(Serialize, Eq, PartialEq, Deserialize, Debug)] pub enum Artifact { Cenotaph(Cenotaph), Runestone(Runestone), } + +impl Artifact { + pub fn mint(&self) -> Option { + match self { + Self::Cenotaph(cenotaph) => cenotaph.mint, + Self::Runestone(runestone) => runestone.mint, + } + } + + pub fn etching(&self) -> Option { + match self { + Self::Cenotaph(cenotaph) => cenotaph.etching, + Self::Runestone(runestone) => runestone.etching.and_then(|etching| etching.rune), + } + } +} diff --git a/crates/ordinals/src/cenotaph.rs b/crates/ordinals/src/cenotaph.rs index d388208fb1..5d245b7019 100644 --- a/crates/ordinals/src/cenotaph.rs +++ b/crates/ordinals/src/cenotaph.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Debug, PartialEq, Default)] +#[derive(Serialize, Eq, PartialEq, Deserialize, Debug, Default)] pub struct Cenotaph { pub flaws: u32, pub mint: Option, diff --git a/crates/ordinals/src/lib.rs b/crates/ordinals/src/lib.rs index d37564eebb..7fb17fd154 100644 --- a/crates/ordinals/src/lib.rs +++ b/crates/ordinals/src/lib.rs @@ -1,4 +1,5 @@ //! Types for interoperating with ordinals, inscriptions, and runes. +#![allow(clippy::large_enum_variant)] use { bitcoin::{ diff --git a/src/index/entry.rs b/src/index/entry.rs index 38c4b44de2..f3cae51f22 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -28,6 +28,18 @@ impl Entry for Header { } } +impl Entry for Rune { + type Value = u128; + + fn load(value: Self::Value) -> Self { + Self(value) + } + + fn store(self) -> Self::Value { + self.0 + } +} + #[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] pub struct RuneEntry { pub block: u64, diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 21c0121d97..254812bb87 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -6,12 +6,8 @@ struct Mint { } struct Etched { - divisibility: u8, id: RuneId, - premine: u128, - spaced_rune: SpacedRune, - symbol: Option, - terms: Option, + rune: Rune, } pub(super) struct RuneUpdater<'a, 'tx, 'client> { @@ -32,38 +28,30 @@ pub(super) struct RuneUpdater<'a, 'tx, 'client> { impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { pub(super) fn index_runes(&mut self, tx_index: u32, tx: &Transaction, txid: Txid) -> Result<()> { - let Artifact::Runestone(runestone) = Runestone::from_transaction(tx) else { - todo!(); - }; + let artifact = Runestone::decipher(tx)?; let mut unallocated = self.unallocated(tx)?; - let cenotaph = runestone - .as_ref() - .map(|runestone| runestone.is_cenotaph()) - .unwrap_or_default(); - - let pointer = runestone.as_ref().and_then(|runestone| runestone.pointer); - let mut allocated: Vec> = vec![HashMap::new(); tx.output.len()]; - if let Some(runestone) = runestone { - if let Some(mint) = runestone - .mint + if let Some(artifact) = &artifact { + if let Some(mint) = artifact + .mint() .and_then(|id| self.mint(id).transpose()) .transpose()? { *unallocated.entry(mint.id).or_default() += mint.amount; } - let etched = self.etched(tx_index, tx, &runestone)?; + let etched = self.etched(tx_index, tx, artifact)?; - if let Some(Etched { id, premine, .. }) = etched { - *unallocated.entry(id).or_default() += premine; - } + if let Artifact::Runestone(runestone) = artifact { + if let Some(Etched { id, .. }) = etched { + *unallocated.entry(id).or_default() += + runestone.etching.unwrap().premine.unwrap_or_default(); + } - if !cenotaph { - for Edict { id, amount, output } in runestone.edicts { + for Edict { id, amount, output } in runestone.edicts.iter().copied() { // edicts with output values greater than the number of outputs // should never be produced by the edict parser let output = usize::try_from(output).unwrap(); @@ -133,17 +121,24 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { } if let Some(etched) = etched { - self.create_rune_entry(txid, cenotaph, etched)?; + self.create_rune_entry(txid, artifact, etched)?; } } let mut burned: HashMap = HashMap::new(); - if cenotaph { + if let Some(Artifact::Cenotaph(_)) = artifact { for (id, balance) in unallocated { *burned.entry(id).or_default() += balance; } } else { + let pointer = artifact + .map(|artifact| match artifact { + Artifact::Runestone(runestone) => runestone.pointer, + Artifact::Cenotaph(_) => unreachable!(), + }) + .unwrap_or_default(); + // 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 @@ -226,20 +221,13 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { Ok(()) } - fn create_rune_entry(&mut self, txid: Txid, burn: bool, etched: Etched) -> Result { - let Etched { - divisibility, - id, - premine, - spaced_rune, - symbol, - terms, - } = etched; - - self.rune_to_id.insert(spaced_rune.rune.0, id.store())?; + fn create_rune_entry(&mut self, txid: Txid, artifact: &Artifact, etched: Etched) -> Result { + let Etched { id, rune } = etched; + + self.rune_to_id.insert(rune.store(), id.store())?; self .transaction_id_to_rune - .insert(&txid.store(), spaced_rune.rune.0)?; + .insert(&txid.store(), rune.store())?; let number = self.runes; self.runes += 1; @@ -248,23 +236,51 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { .statistic_to_count .insert(&Statistic::Runes.into(), self.runes)?; - self.id_to_entry.insert( - id.store(), - RuneEntry { + let entry = match artifact { + Artifact::Cenotaph(_) => RuneEntry { block: id.block, burned: 0, - divisibility, + divisibility: 0, etching: txid, - terms: terms.and_then(|terms| (!burn).then_some(terms)), + terms: None, mints: 0, number, - premine, - spaced_rune, - symbol, + premine: 0, + spaced_rune: SpacedRune { rune, spacers: 0 }, + symbol: None, timestamp: self.block_time.into(), - } - .store(), - )?; + }, + Artifact::Runestone(Runestone { + etching: + Some(Etching { + divisibility, + terms, + premine, + spacers, + symbol, + .. + }), + .. + }) => RuneEntry { + block: id.block, + burned: 0, + divisibility: divisibility.unwrap(), + etching: txid, + terms: *terms, + mints: 0, + number, + premine: premine.unwrap(), + spaced_rune: SpacedRune { + rune, + spacers: spacers.unwrap_or_default(), + }, + symbol: *symbol, + timestamp: self.block_time.into(), + }, + Artifact::Runestone(Runestone { etching: None, .. }) => unreachable!(), + }; + + self.id_to_entry.insert(id.store(), entry.store())?; let inscription_id = InscriptionId { txid, index: 0 }; @@ -284,13 +300,20 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { &mut self, tx_index: u32, tx: &Transaction, - runestone: &Runestone, + artifact: &Artifact, ) -> Result> { - let Some(etching) = runestone.etching else { - return Ok(None); + let rune = match artifact { + Artifact::Runestone(runestone) => match runestone.etching { + Some(etching) => etching.rune, + None => return Ok(None), + }, + Artifact::Cenotaph(cenotaph) => match cenotaph.etching { + Some(rune) => Some(rune), + None => return Ok(None), + }, }; - let rune = if let Some(rune) = etching.rune { + let rune = if let Some(rune) = rune { if rune < self.minimum || rune.is_reserved() || self.rune_to_id.get(rune.0)?.is_some() @@ -314,18 +337,11 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { }; Ok(Some(Etched { - divisibility: etching.divisibility.unwrap_or_default(), id: RuneId { block: self.height.into(), tx: tx_index, }, - premine: etching.premine.unwrap_or_default(), - spaced_rune: SpacedRune { - rune, - spacers: etching.spacers.unwrap_or_default(), - }, - symbol: etching.symbol, - terms: etching.terms, + rune, })) } diff --git a/src/lib.rs b/src/lib.rs index 2343af123f..7feedae4b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,8 +51,8 @@ use { http::HeaderMap, lazy_static::lazy_static, ordinals::{ - varint, Charm, Edict, Epoch, Etching, Height, Pile, Rarity, Rune, RuneId, Runestone, Sat, - SatPoint, SpacedRune, Terms, + varint, Artifact, Charm, Edict, Epoch, Etching, Height, Pile, Rarity, Rune, RuneId, Runestone, + Sat, SatPoint, SpacedRune, Terms, }, regex::Regex, reqwest::Url, diff --git a/src/runes.rs b/src/runes.rs index 1b943dcb3d..79ea98eb30 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -19,6 +19,7 @@ impl Display for MintError { } } +#[cfg(any())] #[cfg(test)] mod tests { use {super::*, crate::index::testing::Context}; diff --git a/src/subcommand/decode.rs b/src/subcommand/decode.rs index bc52f945e2..d80b5dc32e 100644 --- a/src/subcommand/decode.rs +++ b/src/subcommand/decode.rs @@ -3,13 +3,13 @@ use super::*; #[derive(Serialize, Eq, PartialEq, Deserialize, Debug)] pub struct CompactOutput { pub inscriptions: Vec, - pub runestone: Option, + pub runestone: Option, } #[derive(Serialize, Eq, PartialEq, Deserialize, Debug)] pub struct RawOutput { pub inscriptions: Vec, - pub runestone: Option, + pub runestone: Option, } #[derive(Serialize, Eq, PartialEq, Deserialize, Debug)] @@ -84,7 +84,7 @@ impl Decode { let inscriptions = ParsedEnvelope::from_transaction(&transaction); - let runestone = Runestone::from_transaction(&transaction); + let runestone = Runestone::decipher(&transaction)?; if self.compact { Ok(Some(Box::new(CompactOutput { diff --git a/src/subcommand/wallet/mint.rs b/src/subcommand/wallet/mint.rs index c91f5d2972..4512725e42 100644 --- a/src/subcommand/wallet/mint.rs +++ b/src/subcommand/wallet/mint.rs @@ -79,8 +79,8 @@ impl Mint { let signed_transaction = consensus::encode::deserialize(&signed_transaction)?; assert_eq!( - Runestone::from_transaction(&signed_transaction).unwrap(), - runestone, + Runestone::decipher(&signed_transaction), + Ok(Some(Artifact::Runestone(runestone))), ); let transaction = bitcoin_client.send_raw_transaction(&signed_transaction)?; diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index f55c8bb675..7357fa9606 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -307,8 +307,8 @@ impl Send { let unsigned_transaction = consensus::encode::deserialize(&unsigned_transaction)?; assert_eq!( - Runestone::from_transaction(&unsigned_transaction).unwrap(), - runestone, + Runestone::decipher(&unsigned_transaction), + Ok(Some(Artifact::Runestone(runestone))), ); Ok(unsigned_transaction) diff --git a/src/wallet/batch/plan.rs b/src/wallet/batch/plan.rs index 2f09e1ab62..ca8ea1803d 100644 --- a/src/wallet/batch/plan.rs +++ b/src/wallet/batch/plan.rs @@ -459,7 +459,6 @@ impl Plan { } let inner = Runestone { - cenotaph: 0, edicts, etching: Some(ordinals::Etching { divisibility: (etching.divisibility > 0).then_some(etching.divisibility), @@ -649,9 +648,10 @@ impl Plan { let total_fees = Self::calculate_fee(&unsigned_commit_tx, &utxos) + Self::calculate_fee(&reveal_tx, &utxos); - match (Runestone::from_transaction(&reveal_tx), runestone) { + match (Runestone::decipher(&reveal_tx).unwrap(), runestone) { (Some(actual), Some(expected)) => assert_eq!( - actual, expected, + actual, + Artifact::Runestone(expected), "commit transaction runestone did not match expected runestone" ), (Some(_), None) => panic!("commit transaction contained runestone, but none was expected"), diff --git a/tests/decode.rs b/tests/decode.rs index 60d0726e63..20f87c7936 100644 --- a/tests/decode.rs +++ b/tests/decode.rs @@ -67,7 +67,7 @@ fn from_file() { pushnum: false, stutter: false, }], - runestone: Some(Runestone::default()), + runestone: Some(Artifact::Runestone(Runestone::default())), }, ); } @@ -90,7 +90,7 @@ fn from_stdin() { pushnum: false, stutter: false, }], - runestone: Some(Runestone::default()), + runestone: Some(Artifact::Runestone(Runestone::default())), }, ); } @@ -146,7 +146,7 @@ fn compact() { pointer: None, unrecognized_even_field: false, }], - runestone: Some(Runestone::default()), + runestone: Some(Artifact::Runestone(Runestone::default())), }, ); } diff --git a/tests/lib.rs b/tests/lib.rs index c3ff7ec330..70a2b44bbb 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -15,7 +15,9 @@ use { api, chain::Chain, outgoing::Outgoing, subcommand::runes::RuneInfo, wallet::batch, InscriptionId, RuneEntry, }, - ordinals::{Charm, Edict, Pile, Rarity, Rune, RuneId, Runestone, Sat, SatPoint, SpacedRune}, + ordinals::{ + Artifact, Charm, Edict, Pile, Rarity, Rune, RuneId, Runestone, Sat, SatPoint, SpacedRune, + }, pretty_assertions::assert_eq as pretty_assert_eq, regex::Regex, reqwest::{StatusCode, Url}, diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index 3302110248..478de05b70 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -1069,8 +1069,8 @@ fn sending_rune_creates_transaction_with_expected_runestone() { let tx = core.tx_by_id(output.txid); pretty_assert_eq!( - Runestone::from_transaction(&tx).unwrap(), - Runestone { + Runestone::decipher(&tx).unwrap().unwrap(), + Artifact::Runestone(Runestone { pointer: None, etching: None, edicts: vec![Edict { @@ -1078,9 +1078,8 @@ fn sending_rune_creates_transaction_with_expected_runestone() { amount: 750, output: 2 }], - cenotaph: 0, mint: None, - }, + }), ); } From 89deec704ad1b7a9932fe616779e9a3d5785bfe6 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 29 Mar 2024 22:26:53 -0700 Subject: [PATCH 06/15] Fix tests --- src/runes.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/runes.rs b/src/runes.rs index 79ea98eb30..306822fa2a 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -19,7 +19,6 @@ impl Display for MintError { } } -#[cfg(any())] #[cfg(test)] mod tests { use {super::*, crate::index::testing::Context}; @@ -797,8 +796,7 @@ mod tests { rune: Some(Rune(RUNE)), ..default() }), - pointer: None, - cenotaph: 1, + pointer: Some(10), ..default() }, 1, @@ -846,7 +844,7 @@ mod tests { symbol: Some('$'), spacers: Some(1), }), - cenotaph: 1, + pointer: Some(10), ..default() }, 1, @@ -892,7 +890,7 @@ mod tests { output: 0, }], etching: Some(Etching::default()), - cenotaph: 1, + pointer: Some(10), ..default() } .encipher(), @@ -971,7 +969,7 @@ mod tests { inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( Runestone { - cenotaph: 1, + pointer: Some(10), ..default() } .encipher(), @@ -4083,7 +4081,7 @@ mod tests { inputs: &[(5, 0, 0, Witness::new())], op_return: Some( Runestone { - cenotaph: 1, + pointer: Some(10), mint: Some(id), edicts: vec![Edict { id, From 0ffc326388cda8fce99cc026c432a4632d4bf7ef Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 29 Mar 2024 22:32:08 -0700 Subject: [PATCH 07/15] Fix tests --- src/index/updater/rune_updater.rs | 4 ++-- src/runes.rs | 35 ++++++++----------------------- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 254812bb87..47cb748e6c 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -264,12 +264,12 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { }) => RuneEntry { block: id.block, burned: 0, - divisibility: divisibility.unwrap(), + divisibility: divisibility.unwrap_or_default(), etching: txid, terms: *terms, mints: 0, number, - premine: premine.unwrap(), + premine: premine.unwrap_or_default(), spaced_rune: SpacedRune { rune, spacers: spacers.unwrap_or_default(), diff --git a/src/runes.rs b/src/runes.rs index 306822fa2a..5d4664c33f 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -821,7 +821,7 @@ mod tests { } #[test] - fn etched_rune_open_mint_parameters_are_unset_for_cenotaph() { + fn etched_rune_parameters_are_unset_for_cenotaph() { let context = Context::builder().arg("--index-runes").build(); let (txid0, id) = context.etch( @@ -855,18 +855,18 @@ mod tests { id, RuneEntry { block: id.block, - burned: u128::MAX, - divisibility: 1, + burned: 0, + divisibility: 0, etching: txid0, terms: None, mints: 0, number: 0, - premine: u128::MAX, + premine: 0, spaced_rune: SpacedRune { rune: Rune(RUNE), - spacers: 1, + spacers: 0, }, - symbol: Some('$'), + symbol: None, timestamp: id.block, }, )], @@ -875,12 +875,12 @@ mod tests { } #[test] - fn etched_reserved_rune_is_allocated_with_zero_supply_in_cenotaph() { + fn reserved_runes_are_not_allocated_in_cenotaph() { let context = Context::builder().arg("--index-runes").build(); context.mine_blocks(1); - let txid0 = context.core.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Witness::new())], op_return: Some( Runestone { @@ -900,24 +900,7 @@ mod tests { context.mine_blocks(1); - let id = RuneId { block: 2, tx: 1 }; - - context.assert_runes( - [( - id, - RuneEntry { - block: id.block, - etching: txid0, - spaced_rune: SpacedRune { - rune: Rune::reserved(2, 1), - spacers: 0, - }, - timestamp: id.block, - ..default() - }, - )], - [], - ); + context.assert_runes([], []); } #[test] From 83d71a5dd35fe82514980a49652431cecda2d674 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 29 Mar 2024 22:36:56 -0700 Subject: [PATCH 08/15] Modify --- crates/ordinals/src/artifact.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/ordinals/src/artifact.rs b/crates/ordinals/src/artifact.rs index 517a3e44c0..c45cd8355c 100644 --- a/crates/ordinals/src/artifact.rs +++ b/crates/ordinals/src/artifact.rs @@ -13,11 +13,4 @@ impl Artifact { Self::Runestone(runestone) => runestone.mint, } } - - pub fn etching(&self) -> Option { - match self { - Self::Cenotaph(cenotaph) => cenotaph.etching, - Self::Runestone(runestone) => runestone.etching.and_then(|etching| etching.rune), - } - } } From 67ffb6734aaa8920da919594486c087493ae929c Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 29 Mar 2024 22:38:30 -0700 Subject: [PATCH 09/15] Revise --- crates/ordinals/src/cenotaph.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/ordinals/src/cenotaph.rs b/crates/ordinals/src/cenotaph.rs index 5d245b7019..fc1e20117e 100644 --- a/crates/ordinals/src/cenotaph.rs +++ b/crates/ordinals/src/cenotaph.rs @@ -6,3 +6,12 @@ pub struct Cenotaph { pub mint: Option, pub etching: Option, } + +impl Cenotaph { + pub fn flaws(&self) -> Vec { + Flaw::ALL + .into_iter() + .filter(|flaw| self.flaws & flaw.flag() != 0) + .collect() + } +} From bf2bb497250ff95b15af20c4eef867a21ec8ca73 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 29 Mar 2024 22:45:28 -0700 Subject: [PATCH 10/15] Get rid of some structs --- src/index/updater/rune_updater.rs | 50 +++++++++++++------------------ 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 47cb748e6c..2418d52f1b 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -1,15 +1,5 @@ use super::*; -struct Mint { - id: RuneId, - amount: u128, -} - -struct Etched { - id: RuneId, - rune: Rune, -} - pub(super) struct RuneUpdater<'a, 'tx, 'client> { pub(super) block_time: u32, pub(super) burned: HashMap, @@ -35,18 +25,16 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { let mut allocated: Vec> = vec![HashMap::new(); tx.output.len()]; if let Some(artifact) = &artifact { - if let Some(mint) = artifact - .mint() - .and_then(|id| self.mint(id).transpose()) - .transpose()? - { - *unallocated.entry(mint.id).or_default() += mint.amount; + if let Some(id) = artifact.mint() { + if let Some(amount) = self.mint(id)? { + *unallocated.entry(id).or_default() += amount; + } } let etched = self.etched(tx_index, tx, artifact)?; if let Artifact::Runestone(runestone) = artifact { - if let Some(Etched { id, .. }) = etched { + if let Some((id, ..)) = etched { *unallocated.entry(id).or_default() += runestone.etching.unwrap().premine.unwrap_or_default(); } @@ -58,7 +46,7 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { assert!(output <= tx.output.len()); let id = if id == RuneId::default() { - let Some(Etched { id, .. }) = etched else { + let Some((id, ..)) = etched else { continue; }; @@ -120,8 +108,8 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { } } - if let Some(etched) = etched { - self.create_rune_entry(txid, artifact, etched)?; + if let Some((id, rune)) = etched { + self.create_rune_entry(txid, artifact, id, rune)?; } } @@ -221,9 +209,13 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { Ok(()) } - fn create_rune_entry(&mut self, txid: Txid, artifact: &Artifact, etched: Etched) -> Result { - let Etched { id, rune } = etched; - + fn create_rune_entry( + &mut self, + txid: Txid, + artifact: &Artifact, + id: RuneId, + rune: Rune, + ) -> Result { self.rune_to_id.insert(rune.store(), id.store())?; self .transaction_id_to_rune @@ -301,7 +293,7 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { tx_index: u32, tx: &Transaction, artifact: &Artifact, - ) -> Result> { + ) -> Result> { let rune = match artifact { Artifact::Runestone(runestone) => match runestone.etching { Some(etching) => etching.rune, @@ -336,16 +328,16 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { Rune::reserved(self.height.into(), tx_index) }; - Ok(Some(Etched { - id: RuneId { + Ok(Some(( + RuneId { block: self.height.into(), tx: tx_index, }, rune, - })) + ))) } - fn mint(&mut self, id: RuneId) -> Result> { + fn mint(&mut self, id: RuneId) -> Result> { let Some(entry) = self.id_to_entry.get(&id.store())? else { return Ok(None); }; @@ -362,7 +354,7 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { self.id_to_entry.insert(&id.store(), rune_entry.store())?; - Ok(Some(Mint { id, amount })) + Ok(Some(amount)) } fn tx_commits_to_rune(&self, tx: &Transaction, rune: Rune) -> Result { From 13f41cf969157b3cc78c274f21c2449361ccdea7 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 29 Mar 2024 22:52:16 -0700 Subject: [PATCH 11/15] Revise --- src/index/updater/rune_updater.rs | 55 +++++++++++++++---------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 2418d52f1b..f476fedffe 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -242,34 +242,33 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { symbol: None, timestamp: self.block_time.into(), }, - Artifact::Runestone(Runestone { - etching: - Some(Etching { - divisibility, - terms, - premine, - spacers, - symbol, - .. - }), - .. - }) => RuneEntry { - block: id.block, - burned: 0, - divisibility: divisibility.unwrap_or_default(), - etching: txid, - terms: *terms, - mints: 0, - number, - premine: premine.unwrap_or_default(), - spaced_rune: SpacedRune { - rune, - spacers: spacers.unwrap_or_default(), - }, - symbol: *symbol, - timestamp: self.block_time.into(), - }, - Artifact::Runestone(Runestone { etching: None, .. }) => unreachable!(), + Artifact::Runestone(Runestone { etching, .. }) => { + let Etching { + divisibility, + terms, + premine, + spacers, + symbol, + .. + } = etching.unwrap(); + + RuneEntry { + block: id.block, + burned: 0, + divisibility: divisibility.unwrap_or_default(), + etching: txid, + terms, + mints: 0, + number, + premine: premine.unwrap_or_default(), + spaced_rune: SpacedRune { + rune, + spacers: spacers.unwrap_or_default(), + }, + symbol, + timestamp: self.block_time.into(), + } + } }; self.id_to_entry.insert(id.store(), entry.store())?; From a33530a570996de1e908e7469461527143792a7c Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 30 Mar 2024 16:34:17 -0700 Subject: [PATCH 12/15] Adapt --- crates/ordinals/src/runestone.rs | 2 +- tests/wallet/batch_command.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/ordinals/src/runestone.rs b/crates/ordinals/src/runestone.rs index 63c1c972fd..2cfbd46c64 100644 --- a/crates/ordinals/src/runestone.rs +++ b/crates/ordinals/src/runestone.rs @@ -502,7 +502,7 @@ mod tests { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) .push_opcode(Runestone::MAGIC_NUMBER) - .push_opcode(opcodes::all::OP_PUSHNUM_13) + .push_opcode(opcodes::all::OP_PUSHNUM_1) .into_script(), value: 0, },], diff --git a/tests/wallet/batch_command.rs b/tests/wallet/batch_command.rs index 399db299b4..83f2f30ba1 100644 --- a/tests/wallet/batch_command.rs +++ b/tests/wallet/batch_command.rs @@ -1483,7 +1483,9 @@ fn batch_can_etch_rune() { Sequence::from_height(Runestone::COMMIT_INTERVAL) ); - let runestone = Runestone::from_transaction(&reveal).unwrap(); + let Artifact::Runestone(runestone) = Runestone::decipher(&reveal).unwrap().unwrap() else { + panic!(); + }; let pointer = reveal.output.len() - 2; From 4e5d9dfb2174213effd63c835546f76f27279619 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 30 Mar 2024 16:36:47 -0700 Subject: [PATCH 13/15] Adapt --- crates/ordinals/src/cenotaph.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/ordinals/src/cenotaph.rs b/crates/ordinals/src/cenotaph.rs index fc1e20117e..0a97d1490d 100644 --- a/crates/ordinals/src/cenotaph.rs +++ b/crates/ordinals/src/cenotaph.rs @@ -15,3 +15,20 @@ impl Cenotaph { .collect() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn flaws() { + assert_eq!( + Cenotaph { + flaws: Flaw::Opcode.flag() | Flaw::Varint.flag(), + ..default() + } + .flaws(), + vec![Flaw::Opcode, Flaw::Varint], + ); + } +} From 167cf1614d524fe7225c8e7de717a13d6ee4d580 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 30 Mar 2024 16:37:05 -0700 Subject: [PATCH 14/15] Amend --- crates/ordinals/src/cenotaph.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ordinals/src/cenotaph.rs b/crates/ordinals/src/cenotaph.rs index 0a97d1490d..d03eda4cd8 100644 --- a/crates/ordinals/src/cenotaph.rs +++ b/crates/ordinals/src/cenotaph.rs @@ -2,9 +2,9 @@ use super::*; #[derive(Serialize, Eq, PartialEq, Deserialize, Debug, Default)] pub struct Cenotaph { + pub etching: Option, pub flaws: u32, pub mint: Option, - pub etching: Option, } impl Cenotaph { From ef89abc4203d95ccc954d60f683ee2d7f4d44545 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 30 Mar 2024 16:38:31 -0700 Subject: [PATCH 15/15] Enhance --- crates/ordinals/src/runestone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ordinals/src/runestone.rs b/crates/ordinals/src/runestone.rs index 2cfbd46c64..3d0cf5e497 100644 --- a/crates/ordinals/src/runestone.rs +++ b/crates/ordinals/src/runestone.rs @@ -326,7 +326,7 @@ mod tests { lock_time: LockTime::ZERO, version: 2, }) - .is_err(),); + .is_err()); } #[test]