diff --git a/src/index.rs b/src/index.rs index 26f5fe2390..0988a02a3d 100644 --- a/src/index.rs +++ b/src/index.rs @@ -13,7 +13,10 @@ use { chrono::SubsecRound, indicatif::{ProgressBar, ProgressStyle}, log::log_enabled, - redb::{Database, ReadableTable, Table, TableDefinition, WriteStrategy, WriteTransaction}, + redb::{ + Database, MultimapTable, MultimapTableDefinition, ReadableMultimapTable, ReadableTable, Table, + TableDefinition, WriteStrategy, WriteTransaction, + }, std::collections::HashMap, std::io::{BufWriter, Write}, std::sync::atomic::{self, AtomicBool}, @@ -24,7 +27,7 @@ mod fetcher; mod rtx; mod updater; -const SCHEMA_VERSION: u64 = 4; +const SCHEMA_VERSION: u64 = 5; macro_rules! define_table { ($name:ident, $key:ty, $value:ty) => { @@ -32,14 +35,22 @@ macro_rules! define_table { }; } +macro_rules! define_multimap_table { + ($name:ident, $key:ty, $value:ty) => { + const $name: MultimapTableDefinition<$key, $value> = + MultimapTableDefinition::new(stringify!($name)); + }; +} + 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, 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 } -define_table! { SAT_TO_INSCRIPTION_ID, u64, &InscriptionIdValue } +define_table! { REINSCRIPTION_ID_TO_SEQUENCE_NUMBER, &InscriptionIdValue, u64 } +define_multimap_table! { SATPOINT_TO_INSCRIPTION_ID, &SatPointValue, &InscriptionIdValue } +define_multimap_table! { SAT_TO_INSCRIPTION_ID, u64, &InscriptionIdValue } define_table! { SAT_TO_SATPOINT, u64, &SatPointValue } define_table! { STATISTIC_TO_COUNT, u64, u64 } define_table! { WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP, u64, u128 } @@ -198,8 +209,9 @@ impl Index { tx.open_table(INSCRIPTION_ID_TO_SATPOINT)?; tx.open_table(INSCRIPTION_NUMBER_TO_INSCRIPTION_ID)?; tx.open_table(OUTPOINT_TO_VALUE)?; - tx.open_table(SATPOINT_TO_INSCRIPTION_ID)?; - tx.open_table(SAT_TO_INSCRIPTION_ID)?; + tx.open_table(REINSCRIPTION_ID_TO_SEQUENCE_NUMBER)?; + tx.open_multimap_table(SATPOINT_TO_INSCRIPTION_ID)?; + tx.open_multimap_table(SAT_TO_INSCRIPTION_ID)?; tx.open_table(SAT_TO_SATPOINT)?; tx.open_table(WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP)?; @@ -516,8 +528,9 @@ impl Index { self .database .begin_read()? - .open_table(SAT_TO_INSCRIPTION_ID)? + .open_multimap_table(SAT_TO_INSCRIPTION_ID)? .get(&sat.n())? + .next() .map(|inscription_id| Entry::load(*inscription_id.value())), ) } @@ -580,7 +593,7 @@ impl Index { &self .database .begin_read()? - .open_table(SATPOINT_TO_INSCRIPTION_ID)?, + .open_multimap_table(SATPOINT_TO_INSCRIPTION_ID)?, outpoint, )? .map(|(_satpoint, inscription_id)| inscription_id) @@ -588,6 +601,20 @@ impl Index { ) } + #[cfg(test)] + pub(crate) fn get_inscriptions_on_output_ordered( + &self, + outpoint: OutPoint, + ) -> Result> { + let rtx = &self.database.begin_read()?; + + let sat_to_id = rtx.open_multimap_table(SATPOINT_TO_INSCRIPTION_ID)?; + + let re_id_to_seq_num = rtx.open_table(REINSCRIPTION_ID_TO_SEQUENCE_NUMBER)?; + + Self::inscriptions_on_output_ordered(&re_id_to_seq_num, &sat_to_id, outpoint) + } + pub(crate) fn get_transaction(&self, txid: Txid) -> Result> { if txid == self.genesis_block_coinbase_txid { Ok(Some(self.genesis_block_coinbase_transaction.clone())) @@ -727,9 +754,11 @@ impl Index { self .database .begin_read()? - .open_table(SATPOINT_TO_INSCRIPTION_ID)? + .open_multimap_table(SATPOINT_TO_INSCRIPTION_ID)? .range::<&[u8; 44]>(&[0; 44]..)? - .map(|(satpoint, id)| (Entry::load(*satpoint.value()), Entry::load(*id.value()))) + .flat_map(|(satpoint, id_iter)| { + id_iter.map(move |id| (Entry::load(*satpoint.value()), Entry::load(*id.value()))) + }) .take(n.unwrap_or(usize::MAX)) .collect(), ) @@ -832,7 +861,7 @@ impl Index { ) { let rtx = self.database.begin_read().unwrap(); - let satpoint_to_inscription_id = rtx.open_table(SATPOINT_TO_INSCRIPTION_ID).unwrap(); + let satpoint_to_inscription_id = rtx.open_multimap_table(SATPOINT_TO_INSCRIPTION_ID).unwrap(); let inscription_id_to_satpoint = rtx.open_table(INSCRIPTION_ID_TO_SATPOINT).unwrap(); @@ -852,34 +881,22 @@ impl Index { satpoint, ); - assert_eq!( - InscriptionId::load( - *satpoint_to_inscription_id - .get(&satpoint.store()) - .unwrap() - .unwrap() - .value() - ), - inscription_id, - ); + assert!(satpoint_to_inscription_id + .get(&satpoint.store()) + .unwrap() + .any(|id| InscriptionId::load(*id.value()) == inscription_id)); match sat { Some(sat) => { if self.has_sat_index().unwrap() { // unbound inscriptions should not be assigned to a sat assert!(satpoint.outpoint != unbound_outpoint()); - assert_eq!( - InscriptionId::load( - *rtx - .open_table(SAT_TO_INSCRIPTION_ID) - .unwrap() - .get(&sat) - .unwrap() - .unwrap() - .value() - ), - inscription_id, - ); + assert!(rtx + .open_multimap_table(SAT_TO_INSCRIPTION_ID) + .unwrap() + .get(&sat) + .unwrap() + .any(|id| InscriptionId::load(*id.value()) == inscription_id)); // we do not track common sats (only the sat ranges) if !Sat(sat).is_common() { @@ -907,7 +924,7 @@ impl Index { } fn inscriptions_on_output<'a: 'tx, 'tx>( - satpoint_to_id: &'a impl ReadableTable<&'static SatPointValue, &'static InscriptionIdValue>, + satpoint_to_id: &'a impl ReadableMultimapTable<&'static SatPointValue, &'static InscriptionIdValue>, outpoint: OutPoint, ) -> Result + 'tx> { let start = SatPoint { @@ -925,9 +942,34 @@ impl Index { Ok( satpoint_to_id .range::<&[u8; 44]>(&start..=&end)? - .map(|(satpoint, id)| (Entry::load(*satpoint.value()), Entry::load(*id.value()))), + .flat_map(|(satpoint, id_iter)| { + id_iter.map(move |id| (Entry::load(*satpoint.value()), Entry::load(*id.value()))) + }), ) } + + fn inscriptions_on_output_ordered<'a: 'tx, 'tx>( + re_id_to_seq_num: &'a impl ReadableTable<&'static InscriptionIdValue, u64>, + satpoint_to_id: &'a impl ReadableMultimapTable<&'static SatPointValue, &'static InscriptionIdValue>, + outpoint: OutPoint, + ) -> Result> { + let mut result = Self::inscriptions_on_output(satpoint_to_id, outpoint)? + .collect::>(); + + if result.len() <= 1 { + return Ok(result); + } + + result.sort_by_key(|(_satpoint, inscription_id)| { + match re_id_to_seq_num.get(&inscription_id.store()) { + Ok(Some(num)) => num.value(), + Ok(None) => 0, + _ => 0, + } + }); + + Ok(result) + } } #[cfg(test)] @@ -2159,7 +2201,7 @@ mod tests { } #[test] - fn inscriptions_on_same_sat_after_the_first_are_unbound() { + fn inscriptions_on_same_sat_after_the_first_are_not_unbound() { for context in Context::configurations() { context.mine_blocks(1); @@ -2209,10 +2251,13 @@ mod tests { context.index.assert_inscription_location( inscription_id, SatPoint { - outpoint: unbound_outpoint(), + outpoint: OutPoint { + txid: second, + vout: 0, + }, offset: 0, }, - None, // should not be on a sat + Some(50 * COIN_VALUE), ); assert!(context @@ -2723,7 +2768,7 @@ mod tests { } #[test] - fn reinscription_on_cursed_inscription_is_not_cursed_but_unbound() { + fn reinscription_on_cursed_inscription_is_not_cursed() { for context in Context::configurations() { context.mine_blocks(1); context.mine_blocks(1); @@ -2787,10 +2832,10 @@ mod tests { context.index.assert_inscription_location( reinscription_on_cursed, SatPoint { - outpoint: unbound_outpoint(), + outpoint: OutPoint { txid, vout: 0 }, offset: 0, }, - None, + Some(100 * COIN_VALUE), ); assert_eq!( @@ -2804,4 +2849,188 @@ mod tests { ); } } + + #[test] + fn second_reinscription_on_cursed_inscription_is_cursed() { + 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, + }, + Some(100 * COIN_VALUE), + ); + + 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: OutPoint { txid, vout: 0 }, + offset: 0, + }, + Some(100 * COIN_VALUE), + ); + + assert_eq!( + context + .index + .get_inscription_entry(reinscription_on_cursed) + .unwrap() + .unwrap() + .number, + 1 + ); + + let witness = envelope(&[ + b"ord", + &[1], + b"text/plain;charset=utf-8", + &[], + b"second reinscription on cursed", + ]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(4, 1, 0)], + witness, + ..Default::default() + }); + + let second_reinscription_on_cursed = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + context.index.assert_inscription_location( + second_reinscription_on_cursed, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }, + Some(100 * COIN_VALUE), + ); + + assert_eq!( + context + .index + .get_inscription_entry(second_reinscription_on_cursed) + .unwrap() + .unwrap() + .number, + -2 + ); + + assert_eq!( + vec![ + cursed, + reinscription_on_cursed, + second_reinscription_on_cursed + ], + context + .index + .get_inscriptions_on_output_ordered(OutPoint { txid, vout: 0 }) + .unwrap() + .iter() + .map(|(_satpoint, inscription_id)| *inscription_id) + .collect::>() + ) + } + } + + #[test] + fn reinscriptions_on_output_correctly_ordered_and_transferred() { + for context in Context::configurations() { + context.mine_blocks(1); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness: inscription("text/plain;charset=utf-8", "hello").to_witness(), + ..Default::default() + }); + + let first = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0)], + witness: inscription("text/plain;charset=utf-8", "hello").to_witness(), + ..Default::default() + }); + + let second = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(3, 1, 0)], + witness: inscription("text/plain;charset=utf-8", "hello").to_witness(), + ..Default::default() + }); + + let third = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + let location = SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }; + + assert_eq!( + vec![(location, first), (location, second), (location, third)], + context + .index + .get_inscriptions_on_output_ordered(OutPoint { txid, vout: 0 }) + .unwrap() + ) + } + } } diff --git a/src/index/updater.rs b/src/index/updater.rs index 76e6a6ecc9..2def182443 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -408,8 +408,9 @@ impl Updater { let mut inscription_id_to_satpoint = wtx.open_table(INSCRIPTION_ID_TO_SATPOINT)?; let mut inscription_number_to_inscription_id = wtx.open_table(INSCRIPTION_NUMBER_TO_INSCRIPTION_ID)?; - let mut sat_to_inscription_id = wtx.open_table(SAT_TO_INSCRIPTION_ID)?; - let mut satpoint_to_inscription_id = wtx.open_table(SATPOINT_TO_INSCRIPTION_ID)?; + let mut reinscription_id_to_seq_num = wtx.open_table(REINSCRIPTION_ID_TO_SEQUENCE_NUMBER)?; + let mut sat_to_inscription_id = wtx.open_multimap_table(SAT_TO_INSCRIPTION_ID)?; + let mut satpoint_to_inscription_id = wtx.open_multimap_table(SATPOINT_TO_INSCRIPTION_ID)?; let mut statistic_to_count = wtx.open_table(STATISTIC_TO_COUNT)?; let mut lost_sats = statistic_to_count @@ -430,6 +431,7 @@ impl Updater { lost_sats, &mut inscription_number_to_inscription_id, &mut outpoint_to_value, + &mut reinscription_id_to_seq_num, &mut sat_to_inscription_id, &mut satpoint_to_inscription_id, block.header.time, diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 2b9abf7753..1305c3f62a 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -1,4 +1,4 @@ -use super::*; +use {super::*, inscription::Curse}; #[derive(Debug, Clone)] pub(super) struct Flotsam { @@ -31,8 +31,10 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { 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>, - satpoint_to_id: &'a mut Table<'db, 'tx, &'static SatPointValue, &'static InscriptionIdValue>, + reinscription_id_to_seq_num: &'a mut Table<'db, 'tx, &'static InscriptionIdValue, u64>, + sat_to_inscription_id: &'a mut MultimapTable<'db, 'tx, u64, &'static InscriptionIdValue>, + satpoint_to_id: + &'a mut MultimapTable<'db, 'tx, &'static SatPointValue, &'static InscriptionIdValue>, timestamp: u32, pub(super) unbound_inscriptions: u64, value_cache: &'a mut HashMap, @@ -47,8 +49,14 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { lost_sats: u64, 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>, + reinscription_id_to_seq_num: &'a mut Table<'db, 'tx, &'static InscriptionIdValue, u64>, + sat_to_inscription_id: &'a mut MultimapTable<'db, 'tx, u64, &'static InscriptionIdValue>, + satpoint_to_id: &'a mut MultimapTable< + 'db, + 'tx, + &'static SatPointValue, + &'static InscriptionIdValue, + >, timestamp: u32, unbound_inscriptions: u64, value_cache: &'a mut HashMap, @@ -78,6 +86,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { number_to_id, outpoint_to_value, reward: Height(height).subsidy(), + reinscription_id_to_seq_num, sat_to_inscription_id, satpoint_to_id, timestamp, @@ -105,10 +114,12 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { continue; } - // 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)? - { + // find existing inscriptions on input (transfers of inscriptions) + for (old_satpoint, inscription_id) in Index::inscriptions_on_output_ordered( + self.reinscription_id_to_seq_num, + self.satpoint_to_id, + tx_in.previous_output, + )? { let offset = input_value + old_satpoint.offset; floating_inscriptions.push(Flotsam { offset, @@ -116,7 +127,10 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { origin: Origin::Old { old_satpoint }, }); - inscribed_offsets.insert(offset, inscription_id); + inscribed_offsets + .entry(offset) + .and_modify(|(_id, count)| *count += 1) + .or_insert((inscription_id, 0)); } let offset = input_value; @@ -144,32 +158,78 @@ 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_key(&offset) - || inscription.tx_in_offset != 0 - || input_value == 0; - let inscription_id = InscriptionId { txid, index: id_counter, }; + let curse = if inscription.tx_in_index != 0 { + Some(Curse::NotInFirstInput) + } else if inscription.tx_in_offset != 0 { + Some(Curse::NotAtOffsetZero) + } else if inscribed_offsets.contains_key(&offset) { + let seq_num = self + .reinscription_id_to_seq_num + .iter()? + .rev() + .next() + .map(|(_id, number)| number.value() + 1) + .unwrap_or(0); + + let sat = Self::calculate_sat(input_sat_ranges, offset); + log::info!("processing reinscription {inscription_id} on sat {:?}: sequence number {seq_num}, inscribed offsets {:?}", sat, inscribed_offsets); + + // if reinscription track its ordering + self + .reinscription_id_to_seq_num + .insert(&inscription_id.store(), seq_num)?; + + Some(Curse::Reinscription) + } else { + None + }; + + if curse.is_some() { + log::info!("found cursed inscription {inscription_id}: {:?}", curse); + } + + let cursed = if let Some(Curse::Reinscription) = curse { + let first_reinscription = inscribed_offsets + .get(&offset) + .map(|(_id, count)| count == &0) + .unwrap_or(false); + + let initial_inscription_is_cursed = inscribed_offsets + .get(&offset) + .and_then(|(inscription_id, _count)| { + match self.id_to_entry.get(&inscription_id.store()) { + Ok(option) => option.map(|entry| { + let loaded_entry = InscriptionEntry::load(entry.value()); + loaded_entry.number < 0 + }), + Err(_) => None, + } + }) + .unwrap_or(false); + + log::info!("{inscription_id}: is first reinscription: {first_reinscription}, initial inscription is cursed: {initial_inscription_is_cursed}"); + + !(initial_inscription_is_cursed && first_reinscription) + } else { + curse.is_some() + }; + + let unbound = input_value == 0 || inscription.tx_in_offset != 0; + + if curse.is_some() || unbound { + log::info!( + "indexing inscription {inscription_id} with curse {:?} as cursed {} and unbound {}", + curse, + cursed, + unbound + ); + } + floating_inscriptions.push(Flotsam { inscription_id, offset, @@ -284,6 +344,26 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { } } + fn calculate_sat( + input_sat_ranges: Option<&VecDeque<(u64, u64)>>, + input_offset: u64, + ) -> Option { + let mut sat = None; + if let Some(input_sat_ranges) = input_sat_ranges { + let mut offset = 0; + for (start, end) in input_sat_ranges { + let size = end - start; + if offset + size > input_offset { + let n = start + input_offset - offset; + sat = Some(Sat(n)); + break; + } + offset += size; + } + } + sat + } + fn update_inscription_location( &mut self, input_sat_ranges: Option<&VecDeque<(u64, u64)>>, @@ -293,7 +373,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let inscription_id = flotsam.inscription_id.store(); let unbound = match flotsam.origin { Origin::Old { old_satpoint } => { - self.satpoint_to_id.remove(&old_satpoint.store())?; + self.satpoint_to_id.remove_all(&old_satpoint.store())?; false } diff --git a/src/inscription.rs b/src/inscription.rs index abdbdd0483..60365a5245 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -20,6 +20,13 @@ const PROTOCOL_ID: &[u8] = b"ord"; const BODY_TAG: &[u8] = &[]; const CONTENT_TYPE_TAG: &[u8] = &[1]; +#[derive(Debug, PartialEq, Clone)] +pub(crate) enum Curse { + NotInFirstInput, + NotAtOffsetZero, + Reinscription, +} + #[derive(Debug, PartialEq, Clone)] pub(crate) struct Inscription { body: Option>,