From 120e529caae6554fde87f78d70be5008e131eabd Mon Sep 17 00:00:00 2001 From: raphjaph Date: Wed, 24 May 2023 14:12:26 +0200 Subject: [PATCH 01/13] still have to fix and write tests --- src/index.rs | 14 +++++------ src/index/entry.rs | 5 ++-- src/index/updater/inscription_updater.rs | 31 ++++++++++++++++++------ src/inscription.rs | 26 ++++++++++++-------- src/lib.rs | 2 +- src/subcommand/server.rs | 4 +-- src/templates/inscription.rs | 2 +- src/templates/inscriptions.rs | 4 +-- 8 files changed, 56 insertions(+), 32 deletions(-) diff --git a/src/index.rs b/src/index.rs index 6ea644a88d..0f007f78dc 100644 --- a/src/index.rs +++ b/src/index.rs @@ -23,7 +23,7 @@ mod fetcher; mod rtx; mod updater; -const SCHEMA_VERSION: u64 = 3; +const SCHEMA_VERSION: u64 = 4; macro_rules! define_table { ($name:ident, $key:ty, $value:ty) => { @@ -34,7 +34,7 @@ macro_rules! define_table { define_table! { HEIGHT_TO_BLOCK_HASH, u64, &BlockHashValue } define_table! { INSCRIPTION_ID_TO_INSCRIPTION_ENTRY, &InscriptionIdValue, InscriptionEntryValue } define_table! { INSCRIPTION_ID_TO_SATPOINT, &InscriptionIdValue, &SatPointValue } -define_table! { INSCRIPTION_NUMBER_TO_INSCRIPTION_ID, u64, &InscriptionIdValue } +define_table! { INSCRIPTION_NUMBER_TO_INSCRIPTION_ID, i64, &InscriptionIdValue } define_table! { OUTPOINT_TO_SAT_RANGES, &OutPointValue, &[u8] } define_table! { OUTPOINT_TO_VALUE, &OutPointValue, u64} define_table! { SATPOINT_TO_INSCRIPTION_ID, &SatPointValue, &InscriptionIdValue } @@ -498,7 +498,7 @@ impl Index { pub(crate) fn get_inscription_id_by_inscription_number( &self, - n: u64, + n: i64, ) -> Result> { Ok( self @@ -541,7 +541,7 @@ impl Index { Ok( self .get_transaction(inscription_id.txid)? - .and_then(|tx| Inscription::from_transaction(&tx)), + .and_then(|tx| Inscription::from_transaction(&tx).ok()), ) } @@ -726,8 +726,8 @@ impl Index { pub(crate) fn get_latest_inscriptions_with_prev_and_next( &self, n: usize, - from: Option, - ) -> Result<(Vec, Option, Option)> { + from: Option, + ) -> Result<(Vec, Option, Option)> { let rtx = self.database.begin_read()?; let inscription_number_to_inscription_id = @@ -769,7 +769,7 @@ impl Index { Ok((inscriptions, prev, next)) } - pub(crate) fn get_feed_inscriptions(&self, n: usize) -> Result> { + pub(crate) fn get_feed_inscriptions(&self, n: usize) -> Result> { Ok( self .database diff --git a/src/index/entry.rs b/src/index/entry.rs index 15ff3d8ecb..9b9c80de05 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -22,15 +22,16 @@ impl Entry for BlockHash { } } +#[derive(Debug)] pub(crate) struct InscriptionEntry { pub(crate) fee: u64, pub(crate) height: u64, - pub(crate) number: u64, + pub(crate) number: i64, pub(crate) sat: Option, pub(crate) timestamp: u32, } -pub(crate) type InscriptionEntryValue = (u64, u64, u64, u64, u32); +pub(crate) type InscriptionEntryValue = (u64, u64, i64, u64, u32); impl Entry for InscriptionEntry { type Value = InscriptionEntryValue; diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 46fccfa4c6..ee4e8c9c06 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -1,13 +1,15 @@ use super::*; +#[derive(Debug)] pub(super) struct Flotsam { inscription_id: InscriptionId, offset: u64, origin: Origin, } +#[derive(Debug)] enum Origin { - New { fee: u64 }, + New { fee: u64, cursed: bool }, Old { old_satpoint: SatPoint }, } @@ -18,8 +20,9 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { value_receiver: &'a mut Receiver, id_to_entry: &'a mut Table<'db, 'tx, &'static InscriptionIdValue, InscriptionEntryValue>, pub(super) lost_sats: u64, - next_number: u64, - number_to_id: &'a mut Table<'db, 'tx, u64, &'static InscriptionIdValue>, + next_cursed_number: i64, + next_number: i64, + number_to_id: &'a mut Table<'db, 'tx, i64, &'static InscriptionIdValue>, outpoint_to_value: &'a mut Table<'db, 'tx, &'static OutPointValue, u64>, reward: u64, sat_to_inscription_id: &'a mut Table<'db, 'tx, u64, &'static InscriptionIdValue>, @@ -36,7 +39,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { value_receiver: &'a mut Receiver, id_to_entry: &'a mut Table<'db, 'tx, &'static InscriptionIdValue, InscriptionEntryValue>, lost_sats: u64, - number_to_id: &'a mut Table<'db, 'tx, u64, &'static InscriptionIdValue>, + number_to_id: &'a mut Table<'db, 'tx, i64, &'static InscriptionIdValue>, outpoint_to_value: &'a mut Table<'db, 'tx, &'static OutPointValue, u64>, sat_to_inscription_id: &'a mut Table<'db, 'tx, u64, &'static InscriptionIdValue>, satpoint_to_id: &'a mut Table<'db, 'tx, &'static SatPointValue, &'static InscriptionIdValue>, @@ -44,6 +47,12 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { unbound_inscriptions: u64, value_cache: &'a mut HashMap, ) -> Result { + let next_cursed_number = number_to_id + .iter()? + .map(|(number, _id)| number.value() - 1) + .next() + .unwrap_or(-1); + let next_number = number_to_id .iter()? .rev() @@ -58,6 +67,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { value_receiver, id_to_entry, lost_sats, + next_cursed_number, next_number, number_to_id, outpoint_to_value, @@ -111,18 +121,25 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { } } + let (inscription, cursed, unbound) = match Inscription::from_transaction(tx) { + Ok(inscription) => (Some(inscription), false, false), + Err(InscriptionError::UnrecognizedEvenField(inscription)) => (Some(inscription), true, true), + _ => (None, false, false), + }; + if inscriptions.iter().all(|flotsam| flotsam.offset != 0) - && Inscription::from_transaction(tx).is_some() + && inscription.is_some() { let flotsam = Flotsam { inscription_id: txid.into(), offset: 0, origin: Origin::New { fee: input_value - tx.output.iter().map(|txout| txout.value).sum::(), + cursed }, }; - if input_value == 0 { + if input_value == 0 || unbound { self.update_inscription_location( input_sat_ranges, flotsam, @@ -217,7 +234,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { Origin::Old { old_satpoint } => { self.satpoint_to_id.remove(&old_satpoint.store())?; } - Origin::New { fee } => { + Origin::New { fee, cursed } => { self .number_to_id .insert(&self.next_number, &inscription_id)?; diff --git a/src/inscription.rs b/src/inscription.rs index d0fba77016..b106b3a565 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -28,8 +28,8 @@ impl Inscription { Self { content_type, body } } - pub(crate) fn from_transaction(tx: &Transaction) -> Option { - InscriptionParser::parse(&tx.input.get(0)?.witness).ok() + pub(crate) fn from_transaction(tx: &Transaction) -> Result { + InscriptionParser::parse(&tx.input.get(0).ok_or(InscriptionError::NoInscription)?.witness) } pub(crate) fn from_file(chain: Chain, path: impl AsRef) -> Result { @@ -122,13 +122,13 @@ impl Inscription { } #[derive(Debug, PartialEq)] -enum InscriptionError { +pub(crate) enum InscriptionError { EmptyWitness, InvalidInscription, KeyPathSpend, NoInscription, Script(script::Error), - UnrecognizedEvenField, + UnrecognizedEvenField(Inscription), } type Result = std::result::Result; @@ -226,7 +226,10 @@ impl<'a> InscriptionParser<'a> { for tag in fields.keys() { if let Some(lsb) = tag.first() { if lsb % 2 == 0 { - return Err(InscriptionError::UnrecognizedEvenField); + return Err(InscriptionError::UnrecognizedEvenField(Inscription { + body, + content_type, + })); } } } @@ -570,7 +573,7 @@ mod tests { }; assert_eq!( - Inscription::from_transaction(&tx), + Inscription::from_transaction(&tx).ok(), Some(inscription("text/plain;charset=utf-8", "ord")), ); } @@ -597,7 +600,7 @@ mod tests { output: Vec::new(), }; - assert_eq!(Inscription::from_transaction(&tx), None); + assert_eq!(Inscription::from_transaction(&tx).ok(), None); } #[test] @@ -621,7 +624,7 @@ mod tests { }; assert_eq!( - Inscription::from_transaction(&tx), + Inscription::from_transaction(&tx).ok(), Some(inscription("foo", [1; 100])) ); } @@ -734,10 +737,13 @@ mod tests { } #[test] - fn unknown_even_fields_are_invalid() { + fn unknown_even_fields_are_valid_but_unbound() { assert_eq!( InscriptionParser::parse(&envelope(&[b"ord", &[2], &[0]])), - Err(InscriptionError::UnrecognizedEvenField), + Err(InscriptionError::UnrecognizedEvenField(Inscription { + body: None, + content_type: None + })), ); } } diff --git a/src/lib.rs b/src/lib.rs index c3875abefc..d205692a30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ use { epoch::Epoch, height::Height, index::{Index, List}, - inscription::Inscription, + inscription::{Inscription, InscriptionError}, inscription_id::InscriptionId, media::Media, options::Options, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 9b38b27ad4..098ef3bbaa 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -884,7 +884,7 @@ impl Server { async fn inscriptions_from( Extension(page_config): Extension>, Extension(index): Extension>, - Path(from): Path, + Path(from): Path, ) -> ServerResult> { Self::inscriptions_inner(page_config, index, Some(from)).await } @@ -892,7 +892,7 @@ impl Server { async fn inscriptions_inner( page_config: Arc, index: Arc, - from: Option, + from: Option, ) -> ServerResult> { let (inscriptions, prev, next) = index.get_latest_inscriptions_with_prev_and_next(100, from)?; Ok( diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index 9c76b22c38..06837d6d5c 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -8,7 +8,7 @@ pub(crate) struct InscriptionHtml { pub(crate) inscription: Inscription, pub(crate) inscription_id: InscriptionId, pub(crate) next: Option, - pub(crate) number: u64, + pub(crate) number: i64, pub(crate) output: Option, pub(crate) previous: Option, pub(crate) sat: Option, diff --git a/src/templates/inscriptions.rs b/src/templates/inscriptions.rs index 3dc9f7997a..8399e8fa59 100644 --- a/src/templates/inscriptions.rs +++ b/src/templates/inscriptions.rs @@ -3,8 +3,8 @@ use super::*; #[derive(Boilerplate)] pub(crate) struct InscriptionsHtml { pub(crate) inscriptions: Vec, - pub(crate) prev: Option, - pub(crate) next: Option, + pub(crate) prev: Option, + pub(crate) next: Option, } impl PageContent for InscriptionsHtml { From 0e8fc087836c5ae3e7dffeae075d7ad39bd5f809 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Wed, 24 May 2023 16:49:25 +0200 Subject: [PATCH 02/13] tests and all --- src/index.rs | 105 +++++++++++++++++++++++ src/index/updater/inscription_updater.rs | 76 +++++++++------- src/inscription.rs | 22 ++--- src/subcommand/server.rs | 10 +-- src/test.rs | 23 ++++- 5 files changed, 180 insertions(+), 56 deletions(-) diff --git a/src/index.rs b/src/index.rs index 0f007f78dc..f6cf813790 100644 --- a/src/index.rs +++ b/src/index.rs @@ -2279,4 +2279,109 @@ mod tests { ); } } + + #[test] + fn unrecognized_even_field_inscriptions_are_cursed_and_unbound() { + for context in Context::configurations() { + context.mine_blocks(1); + + let witness = envelope(&[ + b"ord", + &[1], + b"text/plain;charset=utf-8", + &[2], + b"bar", + &[4], + b"ord", + ]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness, + ..Default::default() + }); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: unbound_outpoint(), + offset: 0, + }, + None, + ); + + assert_eq!( + context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap() + .number, + -1 + ); + } + } + + #[test] + fn cursed_inscriptions_assigned_negative_numbers() { + for context in Context::configurations() { + context.mine_blocks(1); + + let witness = envelope(&[ + b"ord", + &[1], + b"text/plain;charset=utf-8", + &[2], + b"bar", + &[4], + b"ord", + ]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness, + ..Default::default() + }); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + assert_eq!( + context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap() + .number, + -1 + ); + + let witness = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[66], b"zoo"]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0)], + witness, + ..Default::default() + }); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + assert_eq!( + context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap() + .number, + -2 + ); + } + } } diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index ee4e8c9c06..4950a710db 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -9,8 +9,14 @@ pub(super) struct Flotsam { #[derive(Debug)] enum Origin { - New { fee: u64, cursed: bool }, - Old { old_satpoint: SatPoint }, + New { + fee: u64, + cursed: bool, + unbound: bool, + }, + Old { + old_satpoint: SatPoint, + }, } pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { @@ -127,31 +133,18 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { _ => (None, false, false), }; - if inscriptions.iter().all(|flotsam| flotsam.offset != 0) - && inscription.is_some() - { + if inscriptions.iter().all(|flotsam| flotsam.offset != 0) && inscription.is_some() { let flotsam = Flotsam { inscription_id: txid.into(), offset: 0, origin: Origin::New { fee: input_value - tx.output.iter().map(|txout| txout.value).sum::(), - cursed + cursed, + unbound: unbound || input_value == 0, }, }; - if input_value == 0 || unbound { - self.update_inscription_location( - input_sat_ranges, - flotsam, - SatPoint { - outpoint: unbound_outpoint(), - offset: self.unbound_inscriptions, - }, - )?; - self.unbound_inscriptions += 1; - } else { - inscriptions.push(flotsam); - } + inscriptions.push(flotsam); }; let is_coinbase = tx @@ -230,14 +223,27 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { ) -> Result { let inscription_id = flotsam.inscription_id.store(); - match flotsam.origin { + let unbound = match flotsam.origin { Origin::Old { old_satpoint } => { self.satpoint_to_id.remove(&old_satpoint.store())?; + + false } - Origin::New { fee, cursed } => { - self - .number_to_id - .insert(&self.next_number, &inscription_id)?; + Origin::New { + fee, + cursed, + unbound, + } => { + let number = if cursed { + // This looks awkward + self.next_cursed_number -= 1; + self.next_cursed_number + 1 + } else { + self.next_number += 1; + self.next_number - 1 + }; + + self.number_to_id.insert(number, &inscription_id)?; let mut sat = None; if let Some(input_sat_ranges) = input_sat_ranges { @@ -259,21 +265,31 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { &InscriptionEntry { fee, height: self.height, - number: self.next_number, + number, sat, timestamp: self.timestamp, } .store(), )?; - self.next_number += 1; + unbound } - } + }; - let new_satpoint = new_satpoint.store(); + let satpoint = if unbound { + let new_unbound_satpoint = SatPoint { + outpoint: unbound_outpoint(), + offset: self.unbound_inscriptions, + }; + self.unbound_inscriptions += 1; + + new_unbound_satpoint.store() + } else { + new_satpoint.store() + }; - self.satpoint_to_id.insert(&new_satpoint, &inscription_id)?; - self.id_to_satpoint.insert(&inscription_id, &new_satpoint)?; + self.satpoint_to_id.insert(&satpoint, &inscription_id)?; + self.id_to_satpoint.insert(&inscription_id, &satpoint)?; Ok(()) } diff --git a/src/inscription.rs b/src/inscription.rs index b106b3a565..9b6af7450f 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -29,7 +29,13 @@ impl Inscription { } pub(crate) fn from_transaction(tx: &Transaction) -> Result { - InscriptionParser::parse(&tx.input.get(0).ok_or(InscriptionError::NoInscription)?.witness) + InscriptionParser::parse( + &tx + .input + .get(0) + .ok_or(InscriptionError::NoInscription)? + .witness, + ) } pub(crate) fn from_file(chain: Chain, path: impl AsRef) -> Result { @@ -267,20 +273,6 @@ impl<'a> InscriptionParser<'a> { mod tests { use super::*; - fn envelope(payload: &[&[u8]]) -> Witness { - let mut builder = script::Builder::new() - .push_opcode(opcodes::OP_FALSE) - .push_opcode(opcodes::all::OP_IF); - - for data in payload { - builder = builder.push_slice(data); - } - - let script = builder.push_opcode(opcodes::all::OP_ENDIF).into_script(); - - Witness::from_vec(vec![script.into_bytes(), Vec::new()]) - } - #[test] fn empty() { assert_eq!( diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 098ef3bbaa..7ffac85bce 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -843,15 +843,7 @@ impl Server { ) }; - let previous = if let Some(previous) = entry.number.checked_sub(1) { - Some( - index - .get_inscription_id_by_inscription_number(previous)? - .ok_or_not_found(|| format!("inscription {previous}"))?, - ) - } else { - None - }; + let previous = index.get_inscription_id_by_inscription_number(entry.number - 1)?; let next = index.get_inscription_id_by_inscription_number(entry.number + 1)?; diff --git a/src/test.rs b/src/test.rs index 27a8d45f83..9d46a2aa28 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,6 +1,11 @@ pub(crate) use { - super::*, bitcoin::Witness, pretty_assertions::assert_eq as pretty_assert_eq, std::iter, - test_bitcoincore_rpc::TransactionTemplate, unindent::Unindent, + super::*, + bitcoin::blockdata::{opcodes, script}, + bitcoin::Witness, + pretty_assertions::assert_eq as pretty_assert_eq, + std::iter, + test_bitcoincore_rpc::TransactionTemplate, + unindent::Unindent, }; macro_rules! assert_regex_match { @@ -113,3 +118,17 @@ pub(crate) fn inscription_id(n: u32) -> InscriptionId { format!("{}i{n}", hex.repeat(64)).parse().unwrap() } + +pub(crate) fn envelope(payload: &[&[u8]]) -> Witness { + let mut builder = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF); + + for data in payload { + builder = builder.push_slice(data); + } + + let script = builder.push_opcode(opcodes::all::OP_ENDIF).into_script(); + + Witness::from_vec(vec![script.into_bytes(), Vec::new()]) +} From c4ffb2e9bdd8899ffedf5fa343f47db53b52fbbd Mon Sep 17 00:00:00 2001 From: raphjaph Date: Tue, 30 May 2023 11:37:01 +0200 Subject: [PATCH 03/13] add test for zero value transaction --- src/index.rs | 44 ++++++++++++++++++++++++ src/index/updater/inscription_updater.rs | 10 +++--- src/inscription.rs | 12 ++----- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/index.rs b/src/index.rs index f6cf813790..6662e2c0af 100644 --- a/src/index.rs +++ b/src/index.rs @@ -2384,4 +2384,48 @@ mod tests { ); } } + + #[test] + fn zero_value_transaction_inscription_not_cursed_but_unbound() { + for context in Context::configurations() { + context.mine_blocks(1); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + fee: 50 * 100_000_000, + ..Default::default() + }); + + context.mine_blocks(1); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0)], + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); + + let inscription_id = InscriptionId::from(txid); + + context.mine_blocks(1); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: unbound_outpoint(), + offset: 0, + }, + None, + ); + + assert_eq!( + context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap() + .number, + 0 + ); + } + } } diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 4950a710db..a6ebb7eefd 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -127,13 +127,13 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { } } - let (inscription, cursed, unbound) = match Inscription::from_transaction(tx) { - Ok(inscription) => (Some(inscription), false, false), - Err(InscriptionError::UnrecognizedEvenField(inscription)) => (Some(inscription), true, true), - _ => (None, false, false), + let (has_inscription, cursed, unbound) = match Inscription::from_transaction(tx) { + Ok(_inscription) => (true, false, false), + Err(InscriptionError::UnrecognizedEvenField) => (true, true, true), + _ => (false, false, false), }; - if inscriptions.iter().all(|flotsam| flotsam.offset != 0) && inscription.is_some() { + if inscriptions.iter().all(|flotsam| flotsam.offset != 0) && has_inscription { let flotsam = Flotsam { inscription_id: txid.into(), offset: 0, diff --git a/src/inscription.rs b/src/inscription.rs index 9b6af7450f..af135b3666 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -134,7 +134,7 @@ pub(crate) enum InscriptionError { KeyPathSpend, NoInscription, Script(script::Error), - UnrecognizedEvenField(Inscription), + UnrecognizedEvenField, } type Result = std::result::Result; @@ -232,10 +232,7 @@ impl<'a> InscriptionParser<'a> { for tag in fields.keys() { if let Some(lsb) = tag.first() { if lsb % 2 == 0 { - return Err(InscriptionError::UnrecognizedEvenField(Inscription { - body, - content_type, - })); + return Err(InscriptionError::UnrecognizedEvenField); } } } @@ -732,10 +729,7 @@ mod tests { fn unknown_even_fields_are_valid_but_unbound() { assert_eq!( InscriptionParser::parse(&envelope(&[b"ord", &[2], &[0]])), - Err(InscriptionError::UnrecognizedEvenField(Inscription { - body: None, - content_type: None - })), + Err(InscriptionError::UnrecognizedEvenField), ); } } From ed90dea7484f3753bc6827662fd9365751ac790d Mon Sep 17 00:00:00 2001 From: raphjaph Date: Tue, 30 May 2023 11:47:52 +0200 Subject: [PATCH 04/13] remove awkwardness --- src/index/updater/inscription_updater.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index a6ebb7eefd..d339457052 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -235,12 +235,15 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { unbound, } => { let number = if cursed { - // This looks awkward + let next_cursed_number = self.next_cursed_number; self.next_cursed_number -= 1; - self.next_cursed_number + 1 + + next_cursed_number } else { + let next_number = self.next_number; self.next_number += 1; - self.next_number - 1 + + next_number }; self.number_to_id.insert(number, &inscription_id)?; From 0401da3ad2c3ebe17718c5971ab48493adc64579 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Fri, 2 Jun 2023 18:59:11 +0200 Subject: [PATCH 05/13] cursed --- src/index.rs | 51 +++--- src/index/updater/inscription_updater.rs | 132 ++++++++------ src/inscription.rs | 217 +++++++++++++---------- src/lib.rs | 2 +- src/test.rs | 8 + 5 files changed, 243 insertions(+), 167 deletions(-) diff --git a/src/index.rs b/src/index.rs index 6662e2c0af..5ab6f1662f 100644 --- a/src/index.rs +++ b/src/index.rs @@ -538,11 +538,11 @@ impl Index { return Ok(None); } - Ok( - self - .get_transaction(inscription_id.txid)? - .and_then(|tx| Inscription::from_transaction(&tx).ok()), - ) + Ok(self.get_transaction(inscription_id.txid)?.and_then(|tx| { + Inscription::from_transaction(&tx) + .get(inscription_id.index as usize) + .map(|transaction_inscription| transaction_inscription.inscription.clone()) + })) } pub(crate) fn get_inscriptions_on_output( @@ -852,18 +852,20 @@ impl Index { inscription_id, ); - assert_eq!( - SatPoint::load( - *rtx - .open_table(SAT_TO_SATPOINT) - .unwrap() - .get(&sat) - .unwrap() - .unwrap() - .value() - ), - satpoint, - ); + if !Sat(sat).is_common() && satpoint.outpoint != unbound_outpoint() { + assert_eq!( + SatPoint::load( + *rtx + .open_table(SAT_TO_SATPOINT) + .unwrap() + .get(&sat) + .unwrap() + .unwrap() + .value() + ), + satpoint, + ); + } } } } @@ -2121,7 +2123,7 @@ mod tests { } #[test] - fn inscriptions_on_same_sat_after_the_first_are_ignored() { + fn inscriptions_on_same_sat_after_the_first_are_unbound() { for context in Context::configurations() { context.mine_blocks(1); @@ -2164,15 +2166,14 @@ mod tests { ..Default::default() }); + let inscription_id = InscriptionId::from(second); + context.mine_blocks(1); context.index.assert_inscription_location( inscription_id, SatPoint { - outpoint: OutPoint { - txid: second, - vout: 0, - }, + outpoint: unbound_outpoint(), offset: 0, }, Some(50 * COIN_VALUE), @@ -2182,13 +2183,13 @@ mod tests { .index .get_inscription_entry(second.into()) .unwrap() - .is_none()); + .is_some()); assert!(context .index .get_inscription_by_id(second.into()) .unwrap() - .is_none()); + .is_some()); } } @@ -2281,6 +2282,7 @@ mod tests { } #[test] + #[ignore] fn unrecognized_even_field_inscriptions_are_cursed_and_unbound() { for context in Context::configurations() { context.mine_blocks(1); @@ -2327,6 +2329,7 @@ mod tests { } #[test] + #[ignore] fn cursed_inscriptions_assigned_negative_numbers() { for context in Context::configurations() { context.mine_blocks(1); diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index d339457052..da78efe4ad 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -1,13 +1,13 @@ -use super::*; +use {super::*, std::collections::BTreeSet}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub(super) struct Flotsam { inscription_id: InscriptionId, offset: u64, origin: Origin, } -#[derive(Debug)] +#[derive(Debug, Clone)] enum Origin { New { fee: u64, @@ -92,60 +92,88 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { txid: Txid, input_sat_ranges: Option<&VecDeque<(u64, u64)>>, ) -> Result { - let mut inscriptions = Vec::new(); - + let mut new_inscriptions = Inscription::from_transaction(tx).into_iter().peekable(); + let mut floating_inscriptions = Vec::new(); + let mut inscribed_offsets = BTreeSet::new(); let mut input_value = 0; - for tx_in in &tx.input { + let mut id_counter = 0; + // let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); + + for (input_index, tx_in) in tx.input.iter().enumerate() { + // skip subsidy since no inscriptions possible if tx_in.previous_output.is_null() { input_value += Height(self.height).subsidy(); - } else { - for (old_satpoint, inscription_id) in - Index::inscriptions_on_output(self.satpoint_to_id, tx_in.previous_output)? - { - inscriptions.push(Flotsam { - offset: input_value + old_satpoint.offset, - inscription_id, - origin: Origin::Old { old_satpoint }, - }); - } + continue; + } - input_value += if let Some(value) = self.value_cache.remove(&tx_in.previous_output) { - value - } else if let Some(value) = self - .outpoint_to_value - .remove(&tx_in.previous_output.store())? - { - value.value() - } else { - self.value_receiver.blocking_recv().ok_or_else(|| { - anyhow!( - "failed to get transaction for {}", - tx_in.previous_output.txid - ) - })? - } + // find existing inscriptions on input aka transfers of inscriptions + for (old_satpoint, inscription_id) in + Index::inscriptions_on_output(self.satpoint_to_id, tx_in.previous_output)? + { + let offset = input_value + old_satpoint.offset; + floating_inscriptions.push(Flotsam { + offset, + inscription_id, + origin: Origin::Old { old_satpoint }, + }); + + inscribed_offsets.insert(offset); } - } - let (has_inscription, cursed, unbound) = match Inscription::from_transaction(tx) { - Ok(_inscription) => (true, false, false), - Err(InscriptionError::UnrecognizedEvenField) => (true, true, true), - _ => (false, false, false), - }; + let offset = input_value; - if inscriptions.iter().all(|flotsam| flotsam.offset != 0) && has_inscription { - let flotsam = Flotsam { - inscription_id: txid.into(), - offset: 0, - origin: Origin::New { - fee: input_value - tx.output.iter().map(|txout| txout.value).sum::(), - cursed, - unbound: unbound || input_value == 0, - }, + // different ways to get the utxo set (input amount) + input_value += if let Some(value) = self.value_cache.remove(&tx_in.previous_output) { + value + } else if let Some(value) = self + .outpoint_to_value + .remove(&tx_in.previous_output.store())? + { + value.value() + } else { + self.value_receiver.blocking_recv().ok_or_else(|| { + anyhow!( + "failed to get transaction for {}", + tx_in.previous_output.txid + ) + })? }; - inscriptions.push(flotsam); - }; + // go through all inscriptions in this input + while let Some(inscription) = new_inscriptions.peek() { + if inscription.tx_in_index != input_index as u32 { + break; + } + + // ignore reinscriptions on already inscribed offset (sats) + // For now we do not allow reinscriptions (for example inscriptions in same input or on + // existing inscribed sat + let unbound = + inscribed_offsets.contains(&offset) || inscription.tx_in_offset != 0 || input_value == 0; + + let cursed = inscribed_offsets.contains(&offset) + || inscription.tx_in_index != 0 + || inscription.tx_in_offset != 0; + + let inscription_id = InscriptionId { + txid, + index: id_counter, + }; + + floating_inscriptions.push(Flotsam { + inscription_id, + offset, + origin: Origin::New { + fee: 0, //input_value - total_output_value, // TODO + cursed, + unbound, + }, + }); + + new_inscriptions.next(); + id_counter += 1; + } + } let is_coinbase = tx .input @@ -154,11 +182,11 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { .unwrap_or_default(); if is_coinbase { - inscriptions.append(&mut self.flotsam); + floating_inscriptions.append(&mut self.flotsam); } - inscriptions.sort_by_key(|flotsam| flotsam.offset); - let mut inscriptions = inscriptions.into_iter().peekable(); + floating_inscriptions.sort_by_key(|flotsam| flotsam.offset); + let mut inscriptions = floating_inscriptions.into_iter().peekable(); let mut output_value = 0; for (vout, tx_out) in tx.output.iter().enumerate() { @@ -222,7 +250,6 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { new_satpoint: SatPoint, ) -> Result { let inscription_id = flotsam.inscription_id.store(); - let unbound = match flotsam.origin { Origin::Old { old_satpoint } => { self.satpoint_to_id.remove(&old_satpoint.store())?; @@ -285,7 +312,6 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { offset: self.unbound_inscriptions, }; self.unbound_inscriptions += 1; - new_unbound_satpoint.store() } else { new_satpoint.store() diff --git a/src/inscription.rs b/src/inscription.rs index af135b3666..bd0b8301af 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -11,8 +11,12 @@ use { std::{iter::Peekable, str}, }; +const INSCRIPTION_ENVELOPE: [bitcoin::blockdata::script::Instruction<'static>; 3] = [ + Instruction::PushBytes(&[]), // This is an OP_FALSE + Instruction::Op(opcodes::all::OP_IF), + Instruction::PushBytes(PROTOCOL_ID), +]; const PROTOCOL_ID: &[u8] = b"ord"; - const BODY_TAG: &[u8] = &[]; const CONTENT_TYPE_TAG: &[u8] = &[1]; @@ -22,20 +26,38 @@ pub(crate) struct Inscription { content_type: Option>, } +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct TransactionInscription { + pub(crate) inscription: Inscription, + pub(crate) tx_in_index: u32, + pub(crate) tx_in_offset: u32, +} + impl Inscription { #[cfg(test)] pub(crate) fn new(content_type: Option>, body: Option>) -> Self { Self { content_type, body } } - pub(crate) fn from_transaction(tx: &Transaction) -> Result { - InscriptionParser::parse( - &tx - .input - .get(0) - .ok_or(InscriptionError::NoInscription)? - .witness, - ) + pub(crate) fn from_transaction(tx: &Transaction) -> Vec { + let mut result = Vec::new(); + for (index, tx_in) in tx.input.iter().enumerate() { + let Ok(inscriptions) = InscriptionParser::parse(&tx_in.witness) else { continue }; + + result.extend( + inscriptions + .into_iter() + .enumerate() + .map(|(offset, inscription)| TransactionInscription { + inscription, + tx_in_index: index as u32, + tx_in_offset: offset as u32, + }) + .collect::>(), + ) + } + + result } pub(crate) fn from_file(chain: Chain, path: impl AsRef) -> Result { @@ -144,7 +166,7 @@ struct InscriptionParser<'a> { } impl<'a> InscriptionParser<'a> { - fn parse(witness: &Witness) -> Result { + fn parse(witness: &Witness) -> Result> { if witness.is_empty() { return Err(InscriptionError::EmptyWitness); } @@ -174,19 +196,62 @@ impl<'a> InscriptionParser<'a> { InscriptionParser { instructions: Script::from(Vec::from(script)).instructions().peekable(), } - .parse_script() + .parse_inscriptions() + .into_iter() + .collect() } - fn parse_script(mut self) -> Result { + fn parse_inscriptions(&mut self) -> Vec> { + let mut inscriptions = Vec::new(); loop { - let next = self.advance()?; + let current = self.parse_one_inscription(); + if current == Err(InscriptionError::NoInscription) { + break; + } + inscriptions.push(current); + } + + inscriptions + } + + fn parse_one_inscription(&mut self) -> Result { + self.advance_into_inscription_envelope()?; + + let mut fields = BTreeMap::new(); - if next == Instruction::PushBytes(&[]) { - if let Some(inscription) = self.parse_inscription()? { - return Ok(inscription); + loop { + match self.advance()? { + Instruction::PushBytes(BODY_TAG) => { + let mut body = Vec::new(); + while !self.accept(&Instruction::Op(opcodes::all::OP_ENDIF))? { + body.extend_from_slice(self.expect_push()?); + } + fields.insert(BODY_TAG, body); + break; } + Instruction::PushBytes(tag) => { + if fields.contains_key(tag) { + return Err(InscriptionError::InvalidInscription); + } + fields.insert(tag, self.expect_push()?.to_vec()); + } + Instruction::Op(opcodes::all::OP_ENDIF) => break, + _ => return Err(InscriptionError::InvalidInscription), } } + + let body = fields.remove(BODY_TAG); + let content_type = fields.remove(CONTENT_TYPE_TAG); + + for tag in fields.keys() { + if let Some(lsb) = tag.first() { + if lsb % 2 == 0 { + return Err(InscriptionError::UnrecognizedEvenField); + } + } + } + + Ok(Inscription { body, content_type }) } fn advance(&mut self) -> Result> { @@ -197,50 +262,24 @@ impl<'a> InscriptionParser<'a> { .map_err(InscriptionError::Script) } - fn parse_inscription(&mut self) -> Result> { - if self.advance()? == Instruction::Op(opcodes::all::OP_IF) { - if !self.accept(Instruction::PushBytes(PROTOCOL_ID))? { - return Err(InscriptionError::NoInscription); - } - - let mut fields = BTreeMap::new(); - - loop { - match self.advance()? { - Instruction::PushBytes(BODY_TAG) => { - let mut body = Vec::new(); - while !self.accept(Instruction::Op(opcodes::all::OP_ENDIF))? { - body.extend_from_slice(self.expect_push()?); - } - fields.insert(BODY_TAG, body); - break; - } - Instruction::PushBytes(tag) => { - if fields.contains_key(tag) { - return Err(InscriptionError::InvalidInscription); - } - fields.insert(tag, self.expect_push()?.to_vec()); - } - Instruction::Op(opcodes::all::OP_ENDIF) => break, - _ => return Err(InscriptionError::InvalidInscription), - } + fn advance_into_inscription_envelope(&mut self) -> Result<()> { + loop { + if self.match_instructions(&INSCRIPTION_ENVELOPE)? { + break; } + } - let body = fields.remove(BODY_TAG); - let content_type = fields.remove(CONTENT_TYPE_TAG); + Ok(()) + } - for tag in fields.keys() { - if let Some(lsb) = tag.first() { - if lsb % 2 == 0 { - return Err(InscriptionError::UnrecognizedEvenField); - } - } + fn match_instructions(&mut self, instructions: &[Instruction]) -> Result { + for instruction in instructions { + if &self.advance()? != instruction { + return Ok(false); } - - return Ok(Some(Inscription { body, content_type })); } - Ok(None) + Ok(true) } fn expect_push(&mut self) -> Result<&'a [u8]> { @@ -250,10 +289,10 @@ impl<'a> InscriptionParser<'a> { } } - fn accept(&mut self, instruction: Instruction) -> Result { + fn accept(&mut self, instruction: &Instruction) -> Result { match self.instructions.peek() { Some(Ok(next)) => { - if *next == instruction { + if next == instruction { self.advance()?; Ok(true) } else { @@ -269,7 +308,7 @@ impl<'a> InscriptionParser<'a> { #[cfg(test)] mod tests { use super::*; - + #[test] fn empty() { assert_eq!( @@ -309,7 +348,7 @@ mod tests { Script::new().into_bytes(), Vec::new() ])), - Err(InscriptionError::NoInscription), + Ok(vec![]) ); } @@ -339,7 +378,7 @@ mod tests { &[], b"ord", ])), - Ok(inscription("text/plain;charset=utf-8", "ord")), + Ok(vec![inscription("text/plain;charset=utf-8", "ord")]), ); } @@ -355,7 +394,7 @@ mod tests { &[], b"ord", ])), - Ok(inscription("text/plain;charset=utf-8", "ord")), + Ok(vec![inscription("text/plain;charset=utf-8", "ord")]), ); } @@ -363,10 +402,10 @@ mod tests { fn no_content_tag() { assert_eq!( InscriptionParser::parse(&envelope(&[b"ord", &[1], b"text/plain;charset=utf-8"])), - Ok(Inscription { + Ok(vec![Inscription { content_type: Some(b"text/plain;charset=utf-8".to_vec()), body: None, - }), + }]), ); } @@ -374,10 +413,10 @@ mod tests { fn no_content_type() { assert_eq!( InscriptionParser::parse(&envelope(&[b"ord", &[], b"foo"])), - Ok(Inscription { + Ok(vec![Inscription { content_type: None, body: Some(b"foo".to_vec()), - }), + }]), ); } @@ -392,7 +431,7 @@ mod tests { b"foo", b"bar" ])), - Ok(inscription("text/plain;charset=utf-8", "foobar")), + Ok(vec![inscription("text/plain;charset=utf-8", "foobar")]), ); } @@ -400,7 +439,7 @@ mod tests { fn valid_body_in_zero_pushes() { assert_eq!( InscriptionParser::parse(&envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[]])), - Ok(inscription("text/plain;charset=utf-8", "")), + Ok(vec![inscription("text/plain;charset=utf-8", "")]), ); } @@ -418,7 +457,7 @@ mod tests { &[], &[], ])), - Ok(inscription("text/plain;charset=utf-8", "")), + Ok(vec![inscription("text/plain;charset=utf-8", "")]), ); } @@ -438,7 +477,7 @@ mod tests { assert_eq!( InscriptionParser::parse(&Witness::from_vec(vec![script.into_bytes(), Vec::new()])), - Ok(inscription("text/plain;charset=utf-8", "ord")), + Ok(vec![inscription("text/plain;charset=utf-8", "ord")]), ); } @@ -458,12 +497,12 @@ mod tests { assert_eq!( InscriptionParser::parse(&Witness::from_vec(vec![script.into_bytes(), Vec::new()])), - Ok(inscription("text/plain;charset=utf-8", "ord")), + Ok(vec![inscription("text/plain;charset=utf-8", "ord")]), ); } #[test] - fn valid_ignore_inscriptions_after_first() { + fn do_not_ignore_inscriptions_after_first() { let script = script::Builder::new() .push_opcode(opcodes::OP_FALSE) .push_opcode(opcodes::all::OP_IF) @@ -485,7 +524,7 @@ mod tests { assert_eq!( InscriptionParser::parse(&Witness::from_vec(vec![script.into_bytes(), Vec::new()])), - Ok(inscription("text/plain;charset=utf-8", "foo")), + Ok(vec![inscription("text/plain;charset=utf-8", "foo"), inscription("text/plain;charset=utf-8", "bar")]), ); } @@ -499,7 +538,7 @@ mod tests { &[], &[0b10000000] ])), - Ok(inscription("text/plain;charset=utf-8", [0b10000000])), + Ok(vec![inscription("text/plain;charset=utf-8", [0b10000000])]), ); } @@ -513,7 +552,7 @@ mod tests { assert_eq!( InscriptionParser::parse(&Witness::from_vec(vec![script.into_bytes(), Vec::new()])), - Err(InscriptionError::NoInscription) + Ok(vec![]) ); } @@ -527,7 +566,7 @@ mod tests { assert_eq!( InscriptionParser::parse(&Witness::from_vec(vec![script.into_bytes(), Vec::new()])), - Err(InscriptionError::NoInscription) + Ok(vec![]) ); } @@ -535,7 +574,7 @@ mod tests { fn empty_envelope() { assert_eq!( InscriptionParser::parse(&envelope(&[])), - Err(InscriptionError::NoInscription) + Ok(vec![]) ); } @@ -543,7 +582,7 @@ mod tests { fn wrong_magic_number() { assert_eq!( InscriptionParser::parse(&envelope(&[b"foo"])), - Err(InscriptionError::NoInscription), + Ok(vec![]) ); } @@ -562,13 +601,13 @@ mod tests { }; assert_eq!( - Inscription::from_transaction(&tx).ok(), - Some(inscription("text/plain;charset=utf-8", "ord")), + Inscription::from_transaction(&tx), + vec![transaction_inscription("text/plain;charset=utf-8", "ord", 0, 0)], ); } #[test] - fn do_not_extract_from_second_input() { + fn extract_from_second_input() { let tx = Transaction { version: 0, lock_time: bitcoin::PackedLockTime(0), @@ -589,11 +628,11 @@ mod tests { output: Vec::new(), }; - assert_eq!(Inscription::from_transaction(&tx).ok(), None); + assert_eq!(Inscription::from_transaction(&tx), vec![transaction_inscription("foo", [1; 1040], 1, 0)]); } #[test] - fn do_not_extract_from_second_envelope() { + fn extract_from_second_envelope() { let mut builder = script::Builder::new(); builder = inscription("foo", [1; 100]).append_reveal_script_to_builder(builder); builder = inscription("bar", [1; 100]).append_reveal_script_to_builder(builder); @@ -613,8 +652,8 @@ mod tests { }; assert_eq!( - Inscription::from_transaction(&tx).ok(), - Some(inscription("foo", [1; 100])) + Inscription::from_transaction(&tx), + vec![transaction_inscription("foo", [1; 100], 0, 0), transaction_inscription("bar", [1; 100], 0, 1)] ); } @@ -622,7 +661,7 @@ mod tests { fn inscribe_png() { assert_eq!( InscriptionParser::parse(&envelope(&[b"ord", &[1], b"image/png", &[], &[1; 100]])), - Ok(inscription("image/png", [1; 100])), + Ok(vec![inscription("image/png", [1; 100])]), ); } @@ -687,7 +726,7 @@ mod tests { assert_eq!( InscriptionParser::parse(&witness).unwrap(), - inscription("foo", [1; 1040]), + vec![inscription("foo", [1; 1040])], ); } @@ -707,10 +746,10 @@ mod tests { assert_eq!( InscriptionParser::parse(&witness).unwrap(), - Inscription { + vec![Inscription { content_type: None, body: None, - } + }] ); } @@ -718,10 +757,10 @@ mod tests { fn unknown_odd_fields_are_ignored() { assert_eq!( InscriptionParser::parse(&envelope(&[b"ord", &[3], &[0]])), - Ok(Inscription { + Ok(vec![Inscription { content_type: None, body: None, - }), + }]), ); } diff --git a/src/lib.rs b/src/lib.rs index d205692a30..d522c04575 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ use { epoch::Epoch, height::Height, index::{Index, List}, - inscription::{Inscription, InscriptionError}, + inscription::{Inscription, TransactionInscription, InscriptionError}, inscription_id::InscriptionId, media::Media, options::Options, diff --git a/src/test.rs b/src/test.rs index 9d46a2aa28..78cfe69c1f 100644 --- a/src/test.rs +++ b/src/test.rs @@ -109,6 +109,14 @@ pub(crate) fn inscription(content_type: &str, body: impl AsRef<[u8]>) -> Inscrip Inscription::new(Some(content_type.into()), Some(body.as_ref().into())) } +pub(crate) fn transaction_inscription(content_type: &str, body: impl AsRef<[u8]>, tx_in_index: u32, tx_in_offset: u32) -> TransactionInscription { + TransactionInscription { + inscription: inscription(content_type, body), + tx_in_index, + tx_in_offset, + } +} + pub(crate) fn inscription_id(n: u32) -> InscriptionId { let hex = format!("{n:x}"); From abb7f61229936eabd841e75689821992c53b23a6 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Fri, 2 Jun 2023 19:00:03 +0200 Subject: [PATCH 06/13] fmt --- src/inscription.rs | 36 ++++++++++++++++++++++-------------- src/lib.rs | 2 +- src/test.rs | 7 ++++++- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/inscription.rs b/src/inscription.rs index bd0b8301af..1107064ce4 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -308,7 +308,7 @@ impl<'a> InscriptionParser<'a> { #[cfg(test)] mod tests { use super::*; - + #[test] fn empty() { assert_eq!( @@ -348,7 +348,7 @@ mod tests { Script::new().into_bytes(), Vec::new() ])), - Ok(vec![]) + Ok(vec![]) ); } @@ -524,7 +524,10 @@ mod tests { assert_eq!( InscriptionParser::parse(&Witness::from_vec(vec![script.into_bytes(), Vec::new()])), - Ok(vec![inscription("text/plain;charset=utf-8", "foo"), inscription("text/plain;charset=utf-8", "bar")]), + Ok(vec![ + inscription("text/plain;charset=utf-8", "foo"), + inscription("text/plain;charset=utf-8", "bar") + ]), ); } @@ -572,18 +575,12 @@ mod tests { #[test] fn empty_envelope() { - assert_eq!( - InscriptionParser::parse(&envelope(&[])), - Ok(vec![]) - ); + assert_eq!(InscriptionParser::parse(&envelope(&[])), Ok(vec![])); } #[test] fn wrong_magic_number() { - assert_eq!( - InscriptionParser::parse(&envelope(&[b"foo"])), - Ok(vec![]) - ); + assert_eq!(InscriptionParser::parse(&envelope(&[b"foo"])), Ok(vec![])); } #[test] @@ -602,7 +599,12 @@ mod tests { assert_eq!( Inscription::from_transaction(&tx), - vec![transaction_inscription("text/plain;charset=utf-8", "ord", 0, 0)], + vec![transaction_inscription( + "text/plain;charset=utf-8", + "ord", + 0, + 0 + )], ); } @@ -628,7 +630,10 @@ mod tests { output: Vec::new(), }; - assert_eq!(Inscription::from_transaction(&tx), vec![transaction_inscription("foo", [1; 1040], 1, 0)]); + assert_eq!( + Inscription::from_transaction(&tx), + vec![transaction_inscription("foo", [1; 1040], 1, 0)] + ); } #[test] @@ -653,7 +658,10 @@ mod tests { assert_eq!( Inscription::from_transaction(&tx), - vec![transaction_inscription("foo", [1; 100], 0, 0), transaction_inscription("bar", [1; 100], 0, 1)] + vec![ + transaction_inscription("foo", [1; 100], 0, 0), + transaction_inscription("bar", [1; 100], 0, 1) + ] ); } diff --git a/src/lib.rs b/src/lib.rs index d522c04575..85e526ddd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ use { epoch::Epoch, height::Height, index::{Index, List}, - inscription::{Inscription, TransactionInscription, InscriptionError}, + inscription::{Inscription, InscriptionError, TransactionInscription}, inscription_id::InscriptionId, media::Media, options::Options, diff --git a/src/test.rs b/src/test.rs index 78cfe69c1f..962d4e7aa4 100644 --- a/src/test.rs +++ b/src/test.rs @@ -109,7 +109,12 @@ pub(crate) fn inscription(content_type: &str, body: impl AsRef<[u8]>) -> Inscrip Inscription::new(Some(content_type.into()), Some(body.as_ref().into())) } -pub(crate) fn transaction_inscription(content_type: &str, body: impl AsRef<[u8]>, tx_in_index: u32, tx_in_offset: u32) -> TransactionInscription { +pub(crate) fn transaction_inscription( + content_type: &str, + body: impl AsRef<[u8]>, + tx_in_index: u32, + tx_in_offset: u32, +) -> TransactionInscription { TransactionInscription { inscription: inscription(content_type, body), tx_in_index, From cb3f80b3643d81d1bd6289bd123ffebafcc018e7 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Sat, 3 Jun 2023 12:42:14 +0200 Subject: [PATCH 07/13] fix integration test --- src/index/updater/inscription_updater.rs | 38 +++++++++++++++++++++--- src/inscription.rs | 4 +-- src/lib.rs | 2 +- src/test.rs | 1 + 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index da78efe4ad..8fe89c570e 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -97,7 +97,6 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let mut inscribed_offsets = BTreeSet::new(); let mut input_value = 0; let mut id_counter = 0; - // let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); for (input_index, tx_in) in tx.input.iter().enumerate() { // skip subsidy since no inscriptions possible @@ -141,12 +140,12 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { // go through all inscriptions in this input while let Some(inscription) = new_inscriptions.peek() { - if inscription.tx_in_index != input_index as u32 { + if inscription.tx_in_index != u32::try_from(input_index).unwrap() { break; } // ignore reinscriptions on already inscribed offset (sats) - // For now we do not allow reinscriptions (for example inscriptions in same input or on + // For now we do not allow reinscriptions (for example iscriptions in same input or on // existing inscribed sat let unbound = inscribed_offsets.contains(&offset) || inscription.tx_in_offset != 0 || input_value == 0; @@ -164,7 +163,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { inscription_id, offset, origin: Origin::New { - fee: 0, //input_value - total_output_value, // TODO + fee: 0, cursed, unbound, }, @@ -174,6 +173,37 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { id_counter += 1; } } + + // TODO: normalize over multiple inscriptions per tx and inscription size; make a function + let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); + let mut floating_inscriptions = floating_inscriptions + .into_iter() + .map(|flotsam| { + if let Flotsam { + inscription_id, + offset, + origin: + Origin::New { + fee: _, + cursed, + unbound, + }, + } = flotsam + { + Flotsam { + inscription_id, + offset, + origin: Origin::New { + fee: input_value - total_output_value, + cursed, + unbound, + }, + } + } else { + flotsam + } + }) + .collect::>(); let is_coinbase = tx .input diff --git a/src/inscription.rs b/src/inscription.rs index 1107064ce4..8f5607e4b8 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -50,8 +50,8 @@ impl Inscription { .enumerate() .map(|(offset, inscription)| TransactionInscription { inscription, - tx_in_index: index as u32, - tx_in_offset: offset as u32, + tx_in_index: u32::try_from(index).unwrap(), + tx_in_offset: u32::try_from(offset).unwrap(), }) .collect::>(), ) diff --git a/src/lib.rs b/src/lib.rs index 85e526ddd5..c3875abefc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ use { epoch::Epoch, height::Height, index::{Index, List}, - inscription::{Inscription, InscriptionError, TransactionInscription}, + inscription::Inscription, inscription_id::InscriptionId, media::Media, options::Options, diff --git a/src/test.rs b/src/test.rs index 962d4e7aa4..c0f5cf19c0 100644 --- a/src/test.rs +++ b/src/test.rs @@ -6,6 +6,7 @@ pub(crate) use { std::iter, test_bitcoincore_rpc::TransactionTemplate, unindent::Unindent, + crate::inscription::TransactionInscription, }; macro_rules! assert_regex_match { From 0af05aee43d061886241ae99a9561f9ad1a060ba Mon Sep 17 00:00:00 2001 From: raphjaph Date: Sat, 3 Jun 2023 13:46:57 +0200 Subject: [PATCH 08/13] index tests --- src/index.rs | 283 ++++++++++++++++++++++++------ src/inscription.rs | 2 +- test-bitcoincore-rpc/src/state.rs | 8 +- 3 files changed, 237 insertions(+), 56 deletions(-) diff --git a/src/index.rs b/src/index.rs index 5ab6f1662f..f5674810a1 100644 --- a/src/index.rs +++ b/src/index.rs @@ -2282,28 +2282,25 @@ mod tests { } #[test] - #[ignore] - fn unrecognized_even_field_inscriptions_are_cursed_and_unbound() { + fn zero_value_transaction_inscription_not_cursed_but_unbound() { for context in Context::configurations() { context.mine_blocks(1); - let witness = envelope(&[ - b"ord", - &[1], - b"text/plain;charset=utf-8", - &[2], - b"bar", - &[4], - b"ord", - ]); + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + fee: 50 * 100_000_000, + ..Default::default() + }); + + context.mine_blocks(1); let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0)], - witness, + inputs: &[(2, 1, 0)], + witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId { txid, index: 0 }; + let inscription_id = InscriptionId::from(txid); context.mine_blocks(1); @@ -2323,63 +2320,188 @@ mod tests { .unwrap() .unwrap() .number, - -1 + 0 ); } } #[test] - #[ignore] - fn cursed_inscriptions_assigned_negative_numbers() { + fn multiple_inscriptions_in_same_tx_all_but_first_input_are_cursed() { for context in Context::configurations() { context.mine_blocks(1); + context.mine_blocks(1); + context.mine_blocks(1); - let witness = envelope(&[ - b"ord", - &[1], - b"text/plain;charset=utf-8", - &[2], - b"bar", - &[4], - b"ord", - ]); + let witness = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]); let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0)], + inputs: &[(1, 0, 0), (2, 0, 0), (3, 0, 0)], witness, ..Default::default() }); - let inscription_id = InscriptionId { txid, index: 0 }; + let first = InscriptionId { txid, index: 0 }; + let second = InscriptionId { txid, index: 1 }; + let third = InscriptionId { txid, index: 2 }; context.mine_blocks(1); + context.index.assert_inscription_location( + first, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }, + None, + ); + + context.index.assert_inscription_location( + second, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 50 * COIN_VALUE, + }, + None, + ); + + context.index.assert_inscription_location( + third, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 100 * COIN_VALUE, + }, + None, + ); + assert_eq!( context .index - .get_inscription_entry(inscription_id) + .get_inscription_entry(first) + .unwrap() + .unwrap() + .number, + 0 + ); + + assert_eq!( + context + .index + .get_inscription_entry(second) .unwrap() .unwrap() .number, -1 ); - let witness = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[66], b"zoo"]); + assert_eq!( + context + .index + .get_inscription_entry(third) + .unwrap() + .unwrap() + .number, + -2 + ); + } + } + + #[test] + fn multiple_inscriptions_same_input_all_but_first_are_cursed_and_unbound() { + for context in Context::configurations() { + context.rpc_server.mine_blocks(1); + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice(&[1]) + .push_slice(b"text/plain;charset=utf-8") + .push_slice(&[]) + .push_slice(b"foo") + .push_opcode(opcodes::all::OP_ENDIF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice(&[1]) + .push_slice(b"text/plain;charset=utf-8") + .push_slice(&[]) + .push_slice(b"bar") + .push_opcode(opcodes::all::OP_ENDIF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice(&[1]) + .push_slice(b"text/plain;charset=utf-8") + .push_slice(&[]) + .push_slice(b"qix") + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + let witness = Witness::from_vec(vec![script.into_bytes(), Vec::new()]); let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0)], + inputs: &[(1, 0, 0)], witness, ..Default::default() }); - let inscription_id = InscriptionId { txid, index: 0 }; + let first = InscriptionId { txid, index: 0 }; + let second = InscriptionId { txid, index: 1 }; + let third = InscriptionId { txid, index: 2 }; context.mine_blocks(1); + context.index.assert_inscription_location( + first, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }, + None, + ); + + context.index.assert_inscription_location( + second, + SatPoint { + outpoint: unbound_outpoint(), + offset: 0, + }, + None, + ); + + context.index.assert_inscription_location( + third, + SatPoint { + outpoint: unbound_outpoint(), + offset: 1, + }, + None, + ); + assert_eq!( context .index - .get_inscription_entry(inscription_id) + .get_inscription_entry(first) + .unwrap() + .unwrap() + .number, + 0 + ); + + assert_eq!( + context + .index + .get_inscription_entry(second) + .unwrap() + .unwrap() + .number, + -1 + ); + + assert_eq!( + context + .index + .get_inscription_entry(third) .unwrap() .unwrap() .number, @@ -2389,33 +2511,76 @@ mod tests { } #[test] - fn zero_value_transaction_inscription_not_cursed_but_unbound() { + fn multiple_inscriptions_different_inputs_and_same_inputs() { for context in Context::configurations() { - context.mine_blocks(1); + context.rpc_server.mine_blocks(1); + context.rpc_server.mine_blocks(1); + context.rpc_server.mine_blocks(1); + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice(&[1]) + .push_slice(b"text/plain;charset=utf-8") + .push_slice(&[]) + .push_slice(b"foo") + .push_opcode(opcodes::all::OP_ENDIF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice(&[1]) + .push_slice(b"text/plain;charset=utf-8") + .push_slice(&[]) + .push_slice(b"bar") + .push_opcode(opcodes::all::OP_ENDIF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice(&[1]) + .push_slice(b"text/plain;charset=utf-8") + .push_slice(&[]) + .push_slice(b"qix") + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + let witness = Witness::from_vec(vec![script.into_bytes(), Vec::new()]); - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0)], - fee: 50 * 100_000_000, + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0), (2, 0, 0), (3, 0, 0)], + witness, ..Default::default() }); + let first = InscriptionId { txid, index: 0 }; // normal + let fourth = InscriptionId { txid, index: 3 }; // cursed but bound + let ninth = InscriptionId { txid, index: 8 }; // cursed and unbound + context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0)], - witness: inscription("text/plain", "hello").to_witness(), - ..Default::default() - }); - - let inscription_id = InscriptionId::from(txid); + context.index.assert_inscription_location( + first, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }, + None, + ); - context.mine_blocks(1); + context.index.assert_inscription_location( + fourth, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 50 * COIN_VALUE, + }, + None, + ); context.index.assert_inscription_location( - inscription_id, + ninth, SatPoint { outpoint: unbound_outpoint(), - offset: 0, + offset: 5, }, None, ); @@ -2423,12 +2588,32 @@ mod tests { assert_eq!( context .index - .get_inscription_entry(inscription_id) + .get_inscription_entry(first) .unwrap() .unwrap() .number, 0 ); + + assert_eq!( + context + .index + .get_inscription_entry(fourth) + .unwrap() + .unwrap() + .number, + -3 + ); + + assert_eq!( + context + .index + .get_inscription_entry(ninth) + .unwrap() + .unwrap() + .number, + -8 + ); } } } diff --git a/src/inscription.rs b/src/inscription.rs index 8f5607e4b8..9d6c28d1d6 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -773,7 +773,7 @@ mod tests { } #[test] - fn unknown_even_fields_are_valid_but_unbound() { + fn unknown_even_fields_are_invalid() { assert_eq!( InscriptionParser::parse(&envelope(&[b"ord", &[2], &[0]])), Err(InscriptionError::UnrecognizedEvenField), diff --git a/test-bitcoincore-rpc/src/state.rs b/test-bitcoincore-rpc/src/state.rs index c95ff73b00..29b2d91889 100644 --- a/test-bitcoincore-rpc/src/state.rs +++ b/test-bitcoincore-rpc/src/state.rs @@ -131,18 +131,14 @@ impl State { pub(crate) fn broadcast_tx(&mut self, template: TransactionTemplate) -> Txid { let mut total_value = 0; let mut input = Vec::new(); - for (i, (height, tx, vout)) in template.inputs.iter().enumerate() { + for (height, tx, vout) in template.inputs.iter() { let tx = &self.blocks.get(&self.hashes[*height]).unwrap().txdata[*tx]; total_value += tx.output[*vout].value; input.push(TxIn { previous_output: OutPoint::new(tx.txid(), *vout as u32), script_sig: Script::new(), sequence: Sequence::MAX, - witness: if i == 0 { - template.witness.clone() - } else { - Witness::new() - }, + witness: template.witness.clone(), }); } From 7d5a490026bcd2933d5f388579fbec350b892398 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Sat, 3 Jun 2023 15:38:45 +0200 Subject: [PATCH 09/13] some front-end stuff --- src/subcommand/server.rs | 8 ++++---- src/templates/inscription.rs | 32 ++++++++++++++++++++++++++++++++ templates/inscription.html | 6 ++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 7ffac85bce..9f8d9e3341 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -401,7 +401,7 @@ impl Server { None }; - let output = if outpoint == OutPoint::null() { + let output = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { let mut value = 0; if let Some(List::Unspent(ranges)) = &list { @@ -1629,7 +1629,7 @@ mod tests {
id
{inscription_id}
preview
.*
output
-
0000000000000000000000000000000000000000000000000000000000000000:0
.*" +
0000000000000000000000000000000000000000000000000000000000000000:0 \\(unbound\\)
.*" ), ); } @@ -1637,9 +1637,9 @@ mod tests { #[test] fn unknown_output_returns_404() { TestServer::new().assert_response( - "/output/0000000000000000000000000000000000000000000000000000000000000000:0", + "/output/0000000000000000000000000000000000000000000000000000000000000001:0", StatusCode::NOT_FOUND, - "output 0000000000000000000000000000000000000000000000000000000000000000:0 not found", + "output 0000000000000000000000000000000000000000000000000000000000000001:0 not found", ); } diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index 06837d6d5c..bc7fb0069d 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -183,4 +183,36 @@ mod tests { .unindent() ); } + + #[test] + #[ignore] + fn with_cursed_and_unbound() { + assert_regex_match!( + InscriptionHtml { + chain: Chain::Mainnet, + genesis_fee: 1, + genesis_height: 0, + inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"), + inscription_id: inscription_id(2), + next: None, + number: -1, + output: Some(tx_out(1, address())), + previous: None, + sat: None, + satpoint: SatPoint { outpoint: unbound_outpoint(), offset: 0 }, + timestamp: timestamp(0), + }, + " +

Inscription -1

+ .* +
+ .* +
location
+
0{64}:0:0 (unbound)
+ .* +
+ " + .unindent() + ); + } } diff --git a/templates/inscription.html b/templates/inscription.html index e205f6f6bd..c2a7bc6084 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -48,9 +48,15 @@

Inscription {{ self.number }}

genesis transaction
{{ self.inscription_id.txid }}
location
+%% if self.satpoint.outpoint == unbound_outpoint() { +
{{ self.satpoint }} (unbound)
+
output
+
{{ self.satpoint.outpoint }} (unbound)
+%% } else {
{{ self.satpoint }}
output
{{ self.satpoint.outpoint }}
+%% }
offset
{{ self.satpoint.offset }}
From fbdebbfc23a5dc6da76b56c971b774b819bcc46b Mon Sep 17 00:00:00 2001 From: raphjaph Date: Sat, 3 Jun 2023 15:47:07 +0200 Subject: [PATCH 10/13] fmt and fix test --- src/index.rs | 4 ++-- src/index/updater/inscription_updater.rs | 4 ++-- src/templates/inscription.rs | 10 +++++++--- src/test.rs | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/index.rs b/src/index.rs index f5674810a1..f6ff4e2a84 100644 --- a/src/index.rs +++ b/src/index.rs @@ -2516,7 +2516,7 @@ mod tests { context.rpc_server.mine_blocks(1); context.rpc_server.mine_blocks(1); context.rpc_server.mine_blocks(1); - + let script = script::Builder::new() .push_opcode(opcodes::OP_FALSE) .push_opcode(opcodes::all::OP_IF) @@ -2555,7 +2555,7 @@ mod tests { let first = InscriptionId { txid, index: 0 }; // normal let fourth = InscriptionId { txid, index: 3 }; // cursed but bound let ninth = InscriptionId { txid, index: 8 }; // cursed and unbound - + context.mine_blocks(1); context.index.assert_inscription_location( diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 8fe89c570e..0cf551837b 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -173,8 +173,8 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { id_counter += 1; } } - - // TODO: normalize over multiple inscriptions per tx and inscription size; make a function + + // still have to normalize over multiple inscriptions per tx and inscription size let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); let mut floating_inscriptions = floating_inscriptions .into_iter() diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index bc7fb0069d..b3e52033a9 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -185,7 +185,6 @@ mod tests { } #[test] - #[ignore] fn with_cursed_and_unbound() { assert_regex_match!( InscriptionHtml { @@ -199,7 +198,10 @@ mod tests { output: Some(tx_out(1, address())), previous: None, sat: None, - satpoint: SatPoint { outpoint: unbound_outpoint(), offset: 0 }, + satpoint: SatPoint { + outpoint: unbound_outpoint(), + offset: 0 + }, timestamp: timestamp(0), }, " @@ -208,7 +210,9 @@ mod tests {
.*
location
-
0{64}:0:0 (unbound)
+
0{64}:0:0 \\(unbound\\)
+
output
+
0{64}:0 \\(unbound\\)
.*
" diff --git a/src/test.rs b/src/test.rs index c0f5cf19c0..3ca70c33e0 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,12 +1,12 @@ pub(crate) use { super::*, + crate::inscription::TransactionInscription, bitcoin::blockdata::{opcodes, script}, bitcoin::Witness, pretty_assertions::assert_eq as pretty_assert_eq, std::iter, test_bitcoincore_rpc::TransactionTemplate, unindent::Unindent, - crate::inscription::TransactionInscription, }; macro_rules! assert_regex_match { From f9e611b2a06bb592a19a47e3846fac7dadce4eed Mon Sep 17 00:00:00 2001 From: raphjaph Date: Sun, 4 Jun 2023 10:52:45 +0200 Subject: [PATCH 11/13] addressed the code review --- src/index.rs | 74 +++++++++++++++++++++++- src/index/updater/inscription_updater.rs | 11 ++-- src/inscription.rs | 4 +- src/subcommand/server.rs | 10 ++-- src/templates/inscription.rs | 2 +- templates/inscription.html | 4 ++ 6 files changed, 89 insertions(+), 16 deletions(-) diff --git a/src/index.rs b/src/index.rs index f6ff4e2a84..2ffcb73239 100644 --- a/src/index.rs +++ b/src/index.rs @@ -851,7 +851,8 @@ impl Index { ), inscription_id, ); - + + // we do not track common sats or anything in the unbound output if !Sat(sat).is_common() && satpoint.outpoint != unbound_outpoint() { assert_eq!( SatPoint::load( @@ -2282,6 +2283,7 @@ mod tests { } #[test] + // https://github.com/ordinals/ord/issues/2062 fn zero_value_transaction_inscription_not_cursed_but_unbound() { for context in Context::configurations() { context.mine_blocks(1); @@ -2548,7 +2550,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0), (2, 0, 0), (3, 0, 0)], - witness, + witness, // the witness is replicated over all inputs ..Default::default() }); @@ -2616,4 +2618,72 @@ mod tests { ); } } + + #[test] + fn genesis_fee_distributed_evenly() { + for context in Context::configurations() { + context.rpc_server.mine_blocks(1); + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice(&[1]) + .push_slice(b"text/plain;charset=utf-8") + .push_slice(&[]) + .push_slice(b"foo") + .push_opcode(opcodes::all::OP_ENDIF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice(&[1]) + .push_slice(b"text/plain;charset=utf-8") + .push_slice(&[]) + .push_slice(b"bar") + .push_opcode(opcodes::all::OP_ENDIF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice(&[1]) + .push_slice(b"text/plain;charset=utf-8") + .push_slice(&[]) + .push_slice(b"qix") + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + let witness = Witness::from_vec(vec![script.into_bytes(), Vec::new()]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness, + fee: 33, + ..Default::default() + }); + + let first = InscriptionId { txid, index: 0 }; + let second = InscriptionId { txid, index: 1 }; + + context.mine_blocks(1); + + assert_eq!( + context + .index + .get_inscription_entry(first) + .unwrap() + .unwrap() + .fee, + 11 + ); + + assert_eq!( + context + .index + .get_inscription_entry(second) + .unwrap() + .unwrap() + .fee, + 11 + ); + } + } } diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 0cf551837b..17c89d1a04 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -121,7 +121,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let offset = input_value; - // different ways to get the utxo set (input amount) + // multi-level cache for UTXO set to get to the input amount input_value += if let Some(value) = self.value_cache.remove(&tx_in.previous_output) { value } else if let Some(value) = self @@ -144,9 +144,8 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { break; } - // ignore reinscriptions on already inscribed offset (sats) - // For now we do not allow reinscriptions (for example iscriptions in same input or on - // existing inscribed sat + // In this first part of the cursed inscriptions implementation we ignore reinscriptions. + // This will change once we implement reinscriptions. let unbound = inscribed_offsets.contains(&offset) || inscription.tx_in_offset != 0 || input_value == 0; @@ -174,7 +173,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { } } - // still have to normalize over multiple inscriptions per tx and inscription size + // still have to normalize over inscription size let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); let mut floating_inscriptions = floating_inscriptions .into_iter() @@ -194,7 +193,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { inscription_id, offset, origin: Origin::New { - fee: input_value - total_output_value, + fee: (input_value - total_output_value) / id_counter as u64, cursed, unbound, }, diff --git a/src/inscription.rs b/src/inscription.rs index 9d6c28d1d6..abdbdd0483 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -11,7 +11,7 @@ use { std::{iter::Peekable, str}, }; -const INSCRIPTION_ENVELOPE: [bitcoin::blockdata::script::Instruction<'static>; 3] = [ +const INSCRIPTION_ENVELOPE_HEADER: [bitcoin::blockdata::script::Instruction<'static>; 3] = [ Instruction::PushBytes(&[]), // This is an OP_FALSE Instruction::Op(opcodes::all::OP_IF), Instruction::PushBytes(PROTOCOL_ID), @@ -264,7 +264,7 @@ impl<'a> InscriptionParser<'a> { fn advance_into_inscription_envelope(&mut self) -> Result<()> { loop { - if self.match_instructions(&INSCRIPTION_ENVELOPE)? { + if self.match_instructions(&INSCRIPTION_ENVELOPE_HEADER)? { break; } } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 9f8d9e3341..479ab4e475 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1635,11 +1635,11 @@ mod tests { } #[test] - fn unknown_output_returns_404() { - TestServer::new().assert_response( - "/output/0000000000000000000000000000000000000000000000000000000000000001:0", - StatusCode::NOT_FOUND, - "output 0000000000000000000000000000000000000000000000000000000000000001:0 not found", + fn unbound_output_returns_200() { + TestServer::new().assert_response_regex( + "/output/0000000000000000000000000000000000000000000000000000000000000000:0", + StatusCode::OK, + ".*", ); } diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index b3e52033a9..c19e5bf4b2 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -205,7 +205,7 @@ mod tests { timestamp: timestamp(0), }, " -

Inscription -1

+

Inscription -1 \\(unstable\\)

.*
.* diff --git a/templates/inscription.html b/templates/inscription.html index c2a7bc6084..11b7542ac5 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -1,4 +1,8 @@ +%% if self.number >= 0 {

Inscription {{ self.number }}

+%% } else { +

Inscription {{ self.number }} (unstable)

+%% }
%% if let Some(previous) = self.previous { From 45c3cd7a52d343cebf49a167a9fa0a622b2756f1 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Sun, 4 Jun 2023 10:54:58 +0200 Subject: [PATCH 12/13] placate clippy --- src/index.rs | 6 +++--- src/index/updater/inscription_updater.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.rs b/src/index.rs index 2ffcb73239..a299a92fc1 100644 --- a/src/index.rs +++ b/src/index.rs @@ -851,7 +851,7 @@ impl Index { ), inscription_id, ); - + // we do not track common sats or anything in the unbound output if !Sat(sat).is_common() && satpoint.outpoint != unbound_outpoint() { assert_eq!( @@ -2618,7 +2618,7 @@ mod tests { ); } } - + #[test] fn genesis_fee_distributed_evenly() { for context in Context::configurations() { @@ -2660,7 +2660,7 @@ mod tests { ..Default::default() }); - let first = InscriptionId { txid, index: 0 }; + let first = InscriptionId { txid, index: 0 }; let second = InscriptionId { txid, index: 1 }; context.mine_blocks(1); diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 17c89d1a04..de13b0da47 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -144,7 +144,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { break; } - // In this first part of the cursed inscriptions implementation we ignore reinscriptions. + // In this first part of the cursed inscriptions implementation we ignore reinscriptions. // This will change once we implement reinscriptions. let unbound = inscribed_offsets.contains(&offset) || inscription.tx_in_offset != 0 || input_value == 0; @@ -193,7 +193,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { inscription_id, offset, origin: Origin::New { - fee: (input_value - total_output_value) / id_counter as u64, + fee: (input_value - total_output_value) / u64::from(id_counter), cursed, unbound, }, From bbb11078e6b41095db2cae751c135ebdd39740ec Mon Sep 17 00:00:00 2001 From: raphjaph Date: Sun, 4 Jun 2023 14:51:00 +0200 Subject: [PATCH 13/13] handle edge case of reinscribing a cursed inscription --- src/index.rs | 83 ++++++++++++++++++++++++ src/index/updater/inscription_updater.rs | 30 ++++++--- 2 files changed, 104 insertions(+), 9 deletions(-) diff --git a/src/index.rs b/src/index.rs index a299a92fc1..81c19a0e28 100644 --- a/src/index.rs +++ b/src/index.rs @@ -2686,4 +2686,87 @@ mod tests { ); } } + + #[test] + fn reinscription_on_cursed_inscription_is_not_cursed_but_unbound() { + for context in Context::configurations() { + context.mine_blocks(1); + context.mine_blocks(1); + + let witness = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]); + + let cursed_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0), (2, 0, 0)], + witness, + outputs: 2, + ..Default::default() + }); + + let cursed = InscriptionId { + txid: cursed_txid, + index: 1, + }; + + context.mine_blocks(1); + + context.index.assert_inscription_location( + cursed, + SatPoint { + outpoint: OutPoint { + txid: cursed_txid, + vout: 1, + }, + offset: 0, + }, + None, + ); + + assert_eq!( + context + .index + .get_inscription_entry(cursed) + .unwrap() + .unwrap() + .number, + -1 + ); + + let witness = envelope(&[ + b"ord", + &[1], + b"text/plain;charset=utf-8", + &[], + b"reinscription on cursed", + ]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(3, 1, 1)], + witness, + ..Default::default() + }); + + let reinscription_on_cursed = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + context.index.assert_inscription_location( + reinscription_on_cursed, + SatPoint { + outpoint: unbound_outpoint(), + offset: 0, + }, + None, + ); + + assert_eq!( + context + .index + .get_inscription_entry(reinscription_on_cursed) + .unwrap() + .unwrap() + .number, + 1 + ); + } + } } diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index de13b0da47..89f9435ea7 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -1,4 +1,4 @@ -use {super::*, std::collections::BTreeSet}; +use super::*; #[derive(Debug, Clone)] pub(super) struct Flotsam { @@ -94,7 +94,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { ) -> Result { let mut new_inscriptions = Inscription::from_transaction(tx).into_iter().peekable(); let mut floating_inscriptions = Vec::new(); - let mut inscribed_offsets = BTreeSet::new(); + let mut inscribed_offsets = BTreeMap::new(); let mut input_value = 0; let mut id_counter = 0; @@ -116,7 +116,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { origin: Origin::Old { old_satpoint }, }); - inscribed_offsets.insert(offset); + inscribed_offsets.insert(offset, inscription_id); } let offset = input_value; @@ -144,14 +144,26 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { break; } + let initial_inscription_is_cursed = inscribed_offsets + .get(&offset) + .and_then( + |inscription_id| match self.id_to_entry.get(&inscription_id.store()) { + Ok(option) => option.map(|entry| InscriptionEntry::load(entry.value()).number < 0), + Err(_) => None, + }, + ) + .unwrap_or(false); + + let cursed = !initial_inscription_is_cursed + && (inscription.tx_in_index != 0 + || inscription.tx_in_offset != 0 + || inscribed_offsets.contains_key(&offset)); + // In this first part of the cursed inscriptions implementation we ignore reinscriptions. // This will change once we implement reinscriptions. - let unbound = - inscribed_offsets.contains(&offset) || inscription.tx_in_offset != 0 || input_value == 0; - - let cursed = inscribed_offsets.contains(&offset) - || inscription.tx_in_index != 0 - || inscription.tx_in_offset != 0; + let unbound = inscribed_offsets.contains_key(&offset) + || inscription.tx_in_offset != 0 + || input_value == 0; let inscription_id = InscriptionId { txid,