diff --git a/Cargo.lock b/Cargo.lock index 465e454974..18fbab18b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2224,6 +2224,7 @@ dependencies = [ "ordinals", "pretty_assertions", "redb", + "ref-cast", "regex", "reqwest", "rss", @@ -2581,6 +2582,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + [[package]] name = "regex" version = "1.10.5" diff --git a/Cargo.toml b/Cargo.toml index fa457c2149..1f994b96a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,8 @@ miniscript = "10.0.0" mp4 = "0.14.0" ord-bitcoincore-rpc = "0.17.2" ordinals = { version = "0.0.9", path = "crates/ordinals" } -redb = "2.0.0" +redb = "2.1.1" +ref-cast = "1.0.23" regex = "1.6.0" reqwest = { version = "0.11.27", features = ["blocking", "json"] } rss = "2.0.1" diff --git a/ord.yaml b/ord.yaml index d68010a0e9..c49ef1e6f9 100644 --- a/ord.yaml +++ b/ord.yaml @@ -22,7 +22,6 @@ index_addresses: true index_cache_size: 1000000000 index_runes: true index_sats: true -index_spent_sats: true index_transactions: true integration_test: true no_index_inscriptions: true diff --git a/src/index.rs b/src/index.rs index ebbc2f4873..c880a2de6a 100644 --- a/src/index.rs +++ b/src/index.rs @@ -2,12 +2,13 @@ use { self::{ entry::{ Entry, HeaderValue, InscriptionEntry, InscriptionEntryValue, InscriptionIdValue, - OutPointValue, RuneEntryValue, RuneIdValue, SatPointValue, SatRange, TxOutValue, TxidValue, + OutPointValue, RuneEntryValue, RuneIdValue, SatPointValue, SatRange, TxidValue, }, event::Event, lot::Lot, reorg::Reorg, updater::Updater, + utxo_entry::{ParsedUtxoEntry, UtxoEntry, UtxoEntryBuf}, }, super::*, crate::{ @@ -44,13 +45,13 @@ mod lot; mod reorg; mod rtx; mod updater; +mod utxo_entry; #[cfg(test)] pub(crate) mod testing; -const SCHEMA_VERSION: u64 = 27; +const SCHEMA_VERSION: u64 = 28; -define_multimap_table! { SATPOINT_TO_SEQUENCE_NUMBER, &SatPointValue, u32 } define_multimap_table! { SAT_TO_SEQUENCE_NUMBER, u64, u32 } define_multimap_table! { SEQUENCE_NUMBER_TO_CHILDREN, u32, u32 } define_multimap_table! { SCRIPT_PUBKEY_TO_OUTPOINT, &[u8], OutPointValue } @@ -60,8 +61,7 @@ define_table! { HOME_INSCRIPTIONS, u32, InscriptionIdValue } define_table! { INSCRIPTION_ID_TO_SEQUENCE_NUMBER, InscriptionIdValue, u32 } define_table! { INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER, i32, u32 } define_table! { OUTPOINT_TO_RUNE_BALANCES, &OutPointValue, &[u8] } -define_table! { OUTPOINT_TO_SAT_RANGES, &OutPointValue, &[u8] } -define_table! { OUTPOINT_TO_TXOUT, &OutPointValue, TxOutValue } +define_table! { OUTPOINT_TO_UTXO_ENTRY, &OutPointValue, &UtxoEntry } define_table! { RUNE_ID_TO_RUNE_ENTRY, RuneIdValue, RuneEntryValue } define_table! { RUNE_TO_RUNE_ID, u128, RuneIdValue } define_table! { SAT_TO_SATPOINT, u64, &SatPointValue } @@ -79,19 +79,18 @@ pub(crate) enum Statistic { BlessedInscriptions = 1, Commits = 2, CursedInscriptions = 3, - IndexRunes = 4, - IndexSats = 5, - LostSats = 6, - OutputsTraversed = 7, - ReservedRunes = 8, - Runes = 9, - SatRanges = 10, - UnboundInscriptions = 11, - IndexTransactions = 12, - IndexSpentSats = 13, - InitialSyncTime = 14, - IndexAddresses = 15, - IndexInscriptions = 16, + IndexAddresses = 4, + IndexInscriptions = 5, + IndexRunes = 6, + IndexSats = 7, + IndexTransactions = 8, + InitialSyncTime = 9, + LostSats = 10, + OutputsTraversed = 11, + ReservedRunes = 12, + Runes = 13, + SatRanges = 14, + UnboundInscriptions = 16, } impl Statistic { @@ -195,7 +194,6 @@ pub struct Index { index_inscriptions: bool, index_runes: bool, index_sats: bool, - index_spent_sats: bool, index_transactions: bool, path: PathBuf, settings: Settings, @@ -297,7 +295,6 @@ impl Index { tx.set_durability(durability); - tx.open_multimap_table(SATPOINT_TO_SEQUENCE_NUMBER)?; tx.open_multimap_table(SAT_TO_SEQUENCE_NUMBER)?; tx.open_multimap_table(SCRIPT_PUBKEY_TO_OUTPOINT)?; tx.open_multimap_table(SEQUENCE_NUMBER_TO_CHILDREN)?; @@ -307,7 +304,7 @@ impl Index { tx.open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?; tx.open_table(INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER)?; tx.open_table(OUTPOINT_TO_RUNE_BALANCES)?; - tx.open_table(OUTPOINT_TO_TXOUT)?; + tx.open_table(OUTPOINT_TO_UTXO_ENTRY)?; tx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; tx.open_table(RUNE_TO_RUNE_ID)?; tx.open_table(SAT_TO_SATPOINT)?; @@ -318,13 +315,8 @@ impl Index { tx.open_table(WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP)?; { - let mut outpoint_to_sat_ranges = tx.open_table(OUTPOINT_TO_SAT_RANGES)?; let mut statistics = tx.open_table(STATISTIC_TO_COUNT)?; - if settings.index_sats_raw() { - outpoint_to_sat_ranges.insert(&OutPoint::null().store(), [].as_slice())?; - } - Self::set_statistic( &mut statistics, Statistic::IndexAddresses, @@ -346,13 +338,7 @@ impl Index { Self::set_statistic( &mut statistics, Statistic::IndexSats, - u64::from(settings.index_sats_raw() || settings.index_spent_sats_raw()), - )?; - - Self::set_statistic( - &mut statistics, - Statistic::IndexSpentSats, - u64::from(settings.index_spent_sats_raw()), + u64::from(settings.index_sats_raw()), )?; Self::set_statistic( @@ -418,7 +404,6 @@ impl Index { let index_addresses; let index_runes; let index_sats; - let index_spent_sats; let index_transactions; let index_inscriptions; @@ -429,7 +414,6 @@ impl Index { index_inscriptions = Self::is_statistic_set(&statistics, Statistic::IndexInscriptions)?; index_runes = Self::is_statistic_set(&statistics, Statistic::IndexRunes)?; index_sats = Self::is_statistic_set(&statistics, Statistic::IndexSats)?; - index_spent_sats = Self::is_statistic_set(&statistics, Statistic::IndexSpentSats)?; index_transactions = Self::is_statistic_set(&statistics, Statistic::IndexTransactions)?; } @@ -448,7 +432,6 @@ impl Index { index_addresses, index_runes, index_sats, - index_spent_sats, index_transactions, index_inscriptions, settings: settings.clone(), @@ -458,6 +441,15 @@ impl Index { }) } + /// Unlike normal outpoints, which are added to index on creation and removed + /// when spent, the UTXO entry for special outpoints may be updated. + /// + /// The special outpoints are the null outpoint, which receives lost sats, + /// and the unbound outpoint, which receives unbound inscriptions. + pub fn is_special_outpoint(outpoint: OutPoint) -> bool { + outpoint == OutPoint::null() || outpoint == unbound_outpoint() + } + #[cfg(test)] fn set_durability(&mut self, durability: redb::Durability) { self.durability = durability; @@ -468,7 +460,7 @@ impl Index { self .database .begin_read()? - .open_table(OUTPOINT_TO_TXOUT)? + .open_table(OUTPOINT_TO_UTXO_ENTRY)? .get(&output.store())? .is_some(), ) @@ -615,7 +607,7 @@ impl Index { }) .collect(), tree_height: stats.tree_height(), - utxos_indexed: rtx.open_table(OUTPOINT_TO_SAT_RANGES)?.len()?, + utxos_indexed: rtx.open_table(OUTPOINT_TO_UTXO_ENTRY)?.len()?, } }; @@ -636,9 +628,7 @@ impl Index { .unwrap_or(0), index: self, outputs_cached: 0, - outputs_inserted_since_flush: 0, outputs_traversed: 0, - range_cache: HashMap::new(), sat_ranges_since_flush: 0, }; @@ -681,6 +671,7 @@ impl Index { log::info!("exporting database tables to {filename}"); let sequence_number_to_satpoint = rtx.open_table(SEQUENCE_NUMBER_TO_SATPOINT)?; + let outpoint_to_utxo_entry = rtx.open_table(OUTPOINT_TO_UTXO_ENTRY)?; for result in rtx .open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)? @@ -706,17 +697,31 @@ impl Index { let address = if satpoint.outpoint == unbound_outpoint() { "unbound".to_string() } else { - let output = self - .get_transaction(satpoint.outpoint.txid)? - .unwrap() - .output - .into_iter() - .nth(satpoint.outpoint.vout.try_into().unwrap()) - .unwrap(); + let script_pubkey = if self.index_addresses { + ScriptBuf::from_bytes( + outpoint_to_utxo_entry + .get(&satpoint.outpoint.store())? + .unwrap() + .value() + .parse(self) + .script_pubkey() + .to_vec(), + ) + } else { + self + .get_transaction(satpoint.outpoint.txid)? + .unwrap() + .output + .into_iter() + .nth(satpoint.outpoint.vout.try_into().unwrap()) + .unwrap() + .script_pubkey + }; + self .settings .chain() - .address_from_script(&output.script_pubkey) + .address_from_script(&script_pubkey) .map(|address| address.to_string()) .unwrap_or_else(|e| e.to_string()) }; @@ -1499,12 +1504,12 @@ impl Index { outpoint: OutPoint, ) -> Result> { let rtx = self.database.begin_read()?; - let satpoint_to_sequence_number = rtx.open_multimap_table(SATPOINT_TO_SEQUENCE_NUMBER)?; + let outpoint_to_utxo_entry = rtx.open_table(OUTPOINT_TO_UTXO_ENTRY)?; let sequence_number_to_inscription_entry = rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?; - Self::inscriptions_on_output( - &satpoint_to_sequence_number, + self.inscriptions_on_output( + &outpoint_to_utxo_entry, &sequence_number_to_inscription_entry, outpoint, ) @@ -1564,16 +1569,18 @@ impl Index { return Ok(None); } - let outpoint_to_sat_ranges = rtx.0.open_table(OUTPOINT_TO_SAT_RANGES)?; + let outpoint_to_utxo_entry = rtx.0.open_table(OUTPOINT_TO_UTXO_ENTRY)?; + + for entry in outpoint_to_utxo_entry.iter()? { + let (outpoint, utxo_entry) = entry?; + let sat_ranges = utxo_entry.value().parse(self).sat_ranges(); - for range in outpoint_to_sat_ranges.range::<&[u8; 36]>(&[0; 36]..)? { - let (key, value) = range?; let mut offset = 0; - for chunk in value.value().chunks_exact(11) { + for chunk in sat_ranges.chunks_exact(11) { let (start, end) = SatRange::load(chunk.try_into().unwrap()); if start <= sat && sat < end { return Ok(Some(SatPoint { - outpoint: Entry::load(*key.value()), + outpoint: Entry::load(*outpoint.value()), offset: offset + sat - start, })); } @@ -1601,14 +1608,15 @@ impl Index { return Err(anyhow!("range end is before range start")); }; - let outpoint_to_sat_ranges = rtx.0.open_table(OUTPOINT_TO_SAT_RANGES)?; + let outpoint_to_utxo_entry = rtx.0.open_table(OUTPOINT_TO_UTXO_ENTRY)?; let mut result = Vec::new(); - for range in outpoint_to_sat_ranges.range::<&[u8; 36]>(&[0; 36]..)? { - let (outpoint_entry, sat_ranges_entry) = range?; + for entry in outpoint_to_utxo_entry.iter()? { + let (outpoint, utxo_entry) = entry?; + let sat_ranges = utxo_entry.value().parse(self).sat_ranges(); let mut offset = 0; - for sat_range in sat_ranges_entry.value().chunks_exact(11) { + for sat_range in sat_ranges.chunks_exact(11) { let (start, end) = SatRange::load(sat_range.try_into().unwrap()); if end > range_start && start < range_end { @@ -1619,7 +1627,7 @@ impl Index { start: overlap_start, size: overlap_end - overlap_start, satpoint: SatPoint { - outpoint: Entry::load(*outpoint_entry.value()), + outpoint: Entry::load(*outpoint.value()), offset: offset + overlap_start - start, }, }); @@ -1638,15 +1646,21 @@ impl Index { } pub fn list(&self, outpoint: OutPoint) -> Result>> { + if !self.index_sats { + return Ok(None); + } + Ok( self .database .begin_read()? - .open_table(OUTPOINT_TO_SAT_RANGES)? + .open_table(OUTPOINT_TO_UTXO_ENTRY)? .get(&outpoint.store())? - .map(|outpoint| outpoint.value().to_vec()) - .map(|sat_ranges| { - sat_ranges + .map(|utxo_entry| { + utxo_entry + .value() + .parse(self) + .sat_ranges() .chunks_exact(11) .map(|chunk| SatRange::load(chunk.try_into().unwrap())) .collect::>() @@ -1658,11 +1672,11 @@ impl Index { Ok( outpoint != OutPoint::null() && outpoint != self.settings.chain().genesis_coinbase_outpoint() - && if self.index_addresses { + && if self.index_sats { self .database .begin_read()? - .open_table(OUTPOINT_TO_TXOUT)? + .open_table(OUTPOINT_TO_UTXO_ENTRY)? .get(&outpoint.store())? .is_none() } else { @@ -2117,9 +2131,7 @@ impl Index { ) { let rtx = self.database.begin_read().unwrap(); - let satpoint_to_sequence_number = rtx - .open_multimap_table(SATPOINT_TO_SEQUENCE_NUMBER) - .unwrap(); + let outpoint_to_utxo_entry = rtx.open_table(OUTPOINT_TO_UTXO_ENTRY).unwrap(); let sequence_number_to_satpoint = rtx.open_table(SEQUENCE_NUMBER_TO_SATPOINT).unwrap(); @@ -2131,11 +2143,6 @@ impl Index { .unwrap() .value(); - assert_eq!( - satpoint_to_sequence_number.len().unwrap(), - sequence_number_to_satpoint.len().unwrap(), - ); - assert_eq!( SatPoint::load( *sequence_number_to_satpoint @@ -2147,10 +2154,17 @@ impl Index { satpoint, ); - assert!(satpoint_to_sequence_number - .get(&satpoint.store()) + let utxo_entry = outpoint_to_utxo_entry + .get(&satpoint.outpoint.store()) .unwrap() - .any(|result| result.unwrap().value() == sequence_number)); + .unwrap(); + let parsed_inscriptions = utxo_entry.value().parse(self).parse_inscriptions(); + let satpoint_offsets: Vec = parsed_inscriptions + .iter() + .copied() + .filter_map(|(seq, offset)| (seq == sequence_number).then_some(offset)) + .collect(); + assert!(satpoint_offsets == [satpoint.offset]); match sat { Some(sat) => { @@ -2191,47 +2205,33 @@ impl Index { } fn inscriptions_on_output<'a: 'tx, 'tx>( - satpoint_to_sequence_number: &'a impl ReadableMultimapTable<&'static SatPointValue, u32>, + &self, + outpoint_to_utxo_entry: &'a impl ReadableTable<&'static OutPointValue, &'static UtxoEntry>, sequence_number_to_inscription_entry: &'a impl ReadableTable, outpoint: OutPoint, ) -> Result> { - let start = SatPoint { - outpoint, - offset: 0, + if !self.index_inscriptions { + return Ok(Vec::new()); } - .store(); - let end = SatPoint { - outpoint, - offset: u64::MAX, - } - .store(); + let Some(utxo_entry) = outpoint_to_utxo_entry.get(&outpoint.store())? else { + return Ok(Vec::new()); + }; - let mut inscriptions = Vec::new(); + let mut inscriptions = utxo_entry.value().parse(self).parse_inscriptions(); + + inscriptions.sort_by_key(|(sequence_number, _)| *sequence_number); - for range in satpoint_to_sequence_number.range::<&[u8; 44]>(&start..=&end)? { - let (satpoint, sequence_numbers) = range?; - for sequence_number_result in sequence_numbers { - let sequence_number = sequence_number_result?.value(); + inscriptions + .into_iter() + .map(|(sequence_number, offset)| { let entry = sequence_number_to_inscription_entry .get(sequence_number)? .unwrap(); - inscriptions.push(( - sequence_number, - SatPoint::load(*satpoint.value()), - InscriptionEntry::load(entry.value()).id, - )); - } - } - - inscriptions.sort_by_key(|(sequence_number, _, _)| *sequence_number); - - Ok( - inscriptions - .into_iter() - .map(|(_sequence_number, satpoint, inscription_id)| (satpoint, inscription_id)) - .collect(), - ) + let satpoint = SatPoint { outpoint, offset }; + Ok((satpoint, InscriptionEntry::load(entry.value()).id)) + }) + .collect::>() } pub fn get_address_info(&self, address: &Address) -> Result> { @@ -2283,12 +2283,15 @@ impl Index { } pub(crate) fn get_sat_balances_for_outputs(&self, outputs: &Vec) -> Result { - let outpoint_to_txout = self.database.begin_read()?.open_table(OUTPOINT_TO_TXOUT)?; + let outpoint_to_utxo_entry = self + .database + .begin_read()? + .open_table(OUTPOINT_TO_UTXO_ENTRY)?; let mut acc = 0; for output in outputs { - if let Some(value) = outpoint_to_txout.get(&output.store())? { - acc += TxOut::load(value.value()).value; + if let Some(utxo_entry) = outpoint_to_utxo_entry.get(&output.store())? { + acc += utxo_entry.value().parse(self).total_value(); }; } @@ -3256,7 +3259,13 @@ mod tests { .args(["--index-sats", "--first-inscription-height", "10"]) .build(); - let null_ranges = || context.index.list(OutPoint::null()).unwrap().unwrap(); + let null_ranges = || { + context + .index + .list(OutPoint::null()) + .unwrap() + .unwrap_or_default() + }; assert!(null_ranges().is_empty()); @@ -6194,112 +6203,6 @@ mod tests { } } - #[test] - fn index_spent_sats_retains_spent_sat_range_entries() { - let ranges = { - let context = Context::builder().arg("--index-sats").build(); - - context.mine_blocks(1); - - let outpoint = OutPoint { - txid: context.core.tx(1, 0).into(), - vout: 0, - }; - - let ranges = context.index.list(outpoint).unwrap().unwrap(); - - assert!(!ranges.is_empty()); - - context.core.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Default::default())], - ..default() - }); - - context.mine_blocks(1); - - assert!(context.index.list(outpoint).unwrap().is_none()); - - ranges - }; - - { - let context = Context::builder() - .arg("--index-sats") - .arg("--index-spent-sats") - .build(); - - context.mine_blocks(1); - - let outpoint = OutPoint { - txid: context.core.tx(1, 0).into(), - vout: 0, - }; - - let unspent_ranges = context.index.list(outpoint).unwrap().unwrap(); - - assert!(!unspent_ranges.is_empty()); - - assert_eq!(unspent_ranges, ranges); - - context.core.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Default::default())], - ..default() - }); - - context.mine_blocks(1); - - let spent_ranges = context.index.list(outpoint).unwrap().unwrap(); - - assert_eq!(spent_ranges, ranges); - } - } - - #[test] - fn index_spent_sats_implies_index_sats() { - let context = Context::builder().arg("--index-spent-sats").build(); - - context.mine_blocks(1); - - let outpoint = OutPoint { - txid: context.core.tx(1, 0).into(), - vout: 0, - }; - - context.core.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Default::default())], - ..default() - }); - - context.mine_blocks(1); - - assert!(context.index.list(outpoint).unwrap().is_some()); - } - - #[test] - fn spent_sats_are_retained_after_flush() { - let context = Context::builder().arg("--index-spent-sats").build(); - - context.mine_blocks(1); - - let txid = context.core.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Default::default())], - ..default() - }); - - context.mine_blocks_with_update(1, false); - - let outpoint = OutPoint { txid, vout: 0 }; - - context.core.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Default::default())], - ..default() - }); - - context.mine_blocks(1); - - assert!(context.index.list(outpoint).unwrap().is_some()); - } - #[test] fn is_output_spent() { let context = Context::builder().build(); @@ -6789,4 +6692,12 @@ mod tests { } ); } + + #[test] + fn assert_schema_statistic_key_is_zero() { + // other schema statistic keys may chenge when the schema changes, but for + // good error messages in older versions, the schema statistic key must be + // zero + assert_eq!(Statistic::Schema.key(), 0); + } } diff --git a/src/index/entry.rs b/src/index/entry.rs index 60dacc3969..268bf81f9e 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -432,26 +432,6 @@ impl Entry for OutPoint { } } -pub(super) type TxOutValue = ( - u64, // value - Vec, // script_pubkey -); - -impl Entry for TxOut { - type Value = TxOutValue; - - fn load(value: Self::Value) -> Self { - Self { - value: value.0, - script_pubkey: ScriptBuf::from_bytes(value.1), - } - } - - fn store(self) -> Self::Value { - (self.value, self.script_pubkey.to_bytes()) - } -} - pub(super) type SatPointValue = [u8; 44]; impl Entry for SatPoint { @@ -513,19 +493,6 @@ impl Entry for Txid { mod tests { use super::*; - #[test] - fn txout_entry() { - let txout = TxOut { - value: u64::MAX, - script_pubkey: change(0).script_pubkey(), - }; - - let value = (u64::MAX, change(0).script_pubkey().to_bytes()); - - assert_eq!(txout.clone().store(), value); - assert_eq!(TxOut::load(value), txout); - } - #[test] fn inscription_entry() { let id = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefi0" diff --git a/src/index/updater.rs b/src/index/updater.rs index d494f12176..10ae049b4f 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -36,9 +36,7 @@ pub(crate) struct Updater<'index> { pub(super) height: u32, pub(super) index: &'index Index, pub(super) outputs_cached: u64, - pub(super) outputs_inserted_since_flush: u64, pub(super) outputs_traversed: u64, - pub(super) range_cache: HashMap>, pub(super) sat_ranges_since_flush: u64, } @@ -75,15 +73,13 @@ impl<'index> Updater<'index> { let rx = Self::fetch_blocks_from(self.index, self.height, self.index.index_sats)?; - let (mut output_sender, mut txout_receiver, mut address_txout_receiver) = - Self::spawn_fetcher(self.index)?; + let (mut output_sender, mut txout_receiver) = Self::spawn_fetcher(self.index)?; let mut uncommitted = 0; let mut utxo_cache = HashMap::new(); while let Ok(block) = rx.recv() { self.index_block( &mut output_sender, - &mut address_txout_receiver, &mut txout_receiver, &mut wtx, block, @@ -239,13 +235,7 @@ impl<'index> Updater<'index> { } } - fn spawn_fetcher( - index: &Index, - ) -> Result<( - mpsc::Sender, - broadcast::Receiver, - Option>, - )> { + fn spawn_fetcher(index: &Index) -> Result<(mpsc::Sender, broadcast::Receiver)> { let fetcher = Fetcher::new(&index.settings)?; // A block probably has no more than 20k inputs @@ -258,12 +248,6 @@ impl<'index> Updater<'index> { let (txout_sender, txout_receiver) = broadcast::channel::(CHANNEL_BUFFER_SIZE); - let address_txout_receiver = if index.index_addresses { - Some(txout_sender.subscribe()) - } else { - None - }; - // Default rpcworkqueue in bitcoind is 16, meaning more than 16 concurrent requests will be rejected. // Since we are already requesting blocks on a separate thread, and we don't want to break if anything // else runs a request, we keep this to 12. @@ -321,17 +305,16 @@ impl<'index> Updater<'index> { }) }); - Ok((outpoint_sender, txout_receiver, address_txout_receiver)) + Ok((outpoint_sender, txout_receiver)) } fn index_block( &mut self, output_sender: &mut mpsc::Sender, - address_txout_receiver: &mut Option>, txout_receiver: &mut broadcast::Receiver, wtx: &mut WriteTransaction, block: BlockData, - utxo_cache: &mut HashMap, + utxo_cache: &mut HashMap, ) -> Result<()> { Reorg::detect_reorg(&block, self.height, self.index)?; @@ -346,7 +329,101 @@ impl<'index> Updater<'index> { block.txdata.len() ); - let mut outpoint_to_txout = wtx.open_table(OUTPOINT_TO_TXOUT)?; + let mut height_to_block_header = wtx.open_table(HEIGHT_TO_BLOCK_HEADER)?; + let mut inscription_id_to_sequence_number = + wtx.open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?; + let mut statistic_to_count = wtx.open_table(STATISTIC_TO_COUNT)?; + + if self.index.index_inscriptions || self.index.index_addresses || self.index.index_sats { + self.index_utxo_entries( + &block, + txout_receiver, + output_sender, + utxo_cache, + wtx, + &mut inscription_id_to_sequence_number, + &mut statistic_to_count, + &mut sat_ranges_written, + &mut outputs_in_block, + )?; + } + + if self.index.index_runes && self.height >= self.index.settings.first_rune_height() { + let mut outpoint_to_rune_balances = wtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?; + let mut rune_id_to_rune_entry = wtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; + let mut rune_to_rune_id = wtx.open_table(RUNE_TO_RUNE_ID)?; + let mut sequence_number_to_rune_id = wtx.open_table(SEQUENCE_NUMBER_TO_RUNE_ID)?; + let mut transaction_id_to_rune = wtx.open_table(TRANSACTION_ID_TO_RUNE)?; + + let runes = statistic_to_count + .get(&Statistic::Runes.into())? + .map(|x| x.value()) + .unwrap_or(0); + + let mut rune_updater = RuneUpdater { + event_sender: self.index.event_sender.as_ref(), + block_time: block.header.time, + burned: HashMap::new(), + client: &self.index.client, + height: self.height, + id_to_entry: &mut rune_id_to_rune_entry, + inscription_id_to_sequence_number: &mut inscription_id_to_sequence_number, + minimum: Rune::minimum_at_height( + self.index.settings.chain().network(), + Height(self.height), + ), + outpoint_to_balances: &mut outpoint_to_rune_balances, + rune_to_id: &mut rune_to_rune_id, + runes, + sequence_number_to_rune_id: &mut sequence_number_to_rune_id, + statistic_to_count: &mut statistic_to_count, + transaction_id_to_rune: &mut transaction_id_to_rune, + }; + + for (i, (tx, txid)) in block.txdata.iter().enumerate() { + rune_updater.index_runes(u32::try_from(i).unwrap(), tx, *txid)?; + } + + rune_updater.update()?; + } + + height_to_block_header.insert(&self.height, &block.header.store())?; + + self.height += 1; + self.outputs_traversed += outputs_in_block; + + log::info!( + "Wrote {sat_ranges_written} sat ranges from {outputs_in_block} outputs in {} ms", + (Instant::now() - start).as_millis(), + ); + + Ok(()) + } + + fn index_utxo_entries<'wtx>( + &mut self, + block: &BlockData, + txout_receiver: &mut broadcast::Receiver, + output_sender: &mut mpsc::Sender, + utxo_cache: &mut HashMap, + wtx: &'wtx WriteTransaction, + inscription_id_to_sequence_number: &mut Table<'wtx, (u128, u128, u32), u32>, + statistic_to_count: &mut Table<'wtx, u64, u64>, + sat_ranges_written: &mut u64, + outputs_in_block: &mut u64, + ) -> Result<(), Error> { + let mut height_to_last_sequence_number = wtx.open_table(HEIGHT_TO_LAST_SEQUENCE_NUMBER)?; + let mut home_inscriptions = wtx.open_table(HOME_INSCRIPTIONS)?; + let mut inscription_number_to_sequence_number = + wtx.open_table(INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER)?; + let mut outpoint_to_utxo_entry = wtx.open_table(OUTPOINT_TO_UTXO_ENTRY)?; + let mut sat_to_satpoint = wtx.open_table(SAT_TO_SATPOINT)?; + let mut sat_to_sequence_number = wtx.open_multimap_table(SAT_TO_SEQUENCE_NUMBER)?; + let mut script_pubkey_to_outpoint = wtx.open_multimap_table(SCRIPT_PUBKEY_TO_OUTPOINT)?; + let mut sequence_number_to_children = wtx.open_multimap_table(SEQUENCE_NUMBER_TO_CHILDREN)?; + let mut sequence_number_to_inscription_entry = + wtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?; + let mut transaction_id_to_transaction = wtx.open_table(TRANSACTION_ID_TO_TRANSACTION)?; let index_inscriptions = self.height >= self.index.first_inscription_height && self.index.index_inscriptions; @@ -360,14 +437,7 @@ impl<'index> Updater<'index> { ); } - if let Some(receiver) = address_txout_receiver { - assert!( - matches!(receiver.try_recv(), Err(TryRecvError::Empty)), - "Previous block did not consume all inputs" - ); - } - - if index_inscriptions || self.index.index_addresses { + if !self.index.index_sats { // Send all missing input outpoints to be fetched let txids = block .txdata @@ -392,7 +462,7 @@ impl<'index> Updater<'index> { continue; } // We don't need inputs we already have in our database - if outpoint_to_txout.get(&prev_output.store())?.is_some() { + if outpoint_to_utxo_entry.get(&prev_output.store())?.is_some() { continue; } // Send this outpoint to background thread to be fetched @@ -401,37 +471,6 @@ impl<'index> Updater<'index> { } } - if let Some(address_txout_receiver) = address_txout_receiver { - let mut script_pubkey_to_outpoint = wtx.open_multimap_table(SCRIPT_PUBKEY_TO_OUTPOINT)?; - for (tx, txid) in &block.txdata { - self.index_transaction_output_script_pubkeys( - tx, - txid, - address_txout_receiver, - utxo_cache, - &mut script_pubkey_to_outpoint, - &mut outpoint_to_txout, - index_inscriptions, - )?; - } - }; - - let mut height_to_block_header = wtx.open_table(HEIGHT_TO_BLOCK_HEADER)?; - let mut height_to_last_sequence_number = wtx.open_table(HEIGHT_TO_LAST_SEQUENCE_NUMBER)?; - let mut home_inscriptions = wtx.open_table(HOME_INSCRIPTIONS)?; - let mut inscription_id_to_sequence_number = - wtx.open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?; - let mut inscription_number_to_sequence_number = - wtx.open_table(INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER)?; - let mut sat_to_sequence_number = wtx.open_multimap_table(SAT_TO_SEQUENCE_NUMBER)?; - let mut satpoint_to_sequence_number = wtx.open_multimap_table(SATPOINT_TO_SEQUENCE_NUMBER)?; - let mut sequence_number_to_children = wtx.open_multimap_table(SEQUENCE_NUMBER_TO_CHILDREN)?; - let mut sequence_number_to_inscription_entry = - wtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?; - let mut sequence_number_to_satpoint = wtx.open_table(SEQUENCE_NUMBER_TO_SATPOINT)?; - let mut statistic_to_count = wtx.open_table(STATISTIC_TO_COUNT)?; - let mut transaction_id_to_transaction = wtx.open_table(TRANSACTION_ID_TO_TRANSACTION)?; - let mut lost_sats = statistic_to_count .get(&Statistic::LostSats.key())? .map(|lost_sats| lost_sats.value()) @@ -463,134 +502,191 @@ impl<'index> Updater<'index> { let mut inscription_updater = InscriptionUpdater { blessed_inscription_count, - chain: self.index.settings.chain(), cursed_inscription_count, - event_sender: self.index.event_sender.as_ref(), flotsam: Vec::new(), height: self.height, home_inscription_count, home_inscriptions: &mut home_inscriptions, - id_to_sequence_number: &mut inscription_id_to_sequence_number, - index_addresses: self.index.index_addresses, - index_transactions: self.index.index_transactions, + id_to_sequence_number: inscription_id_to_sequence_number, inscription_number_to_sequence_number: &mut inscription_number_to_sequence_number, lost_sats, next_sequence_number, - outpoint_to_txout: &mut outpoint_to_txout, reward: Height(self.height).subsidy(), sat_to_sequence_number: &mut sat_to_sequence_number, - satpoint_to_sequence_number: &mut satpoint_to_sequence_number, sequence_number_to_children: &mut sequence_number_to_children, sequence_number_to_entry: &mut sequence_number_to_inscription_entry, - sequence_number_to_satpoint: &mut sequence_number_to_satpoint, timestamp: block.header.time, transaction_buffer: Vec::new(), transaction_id_to_transaction: &mut transaction_id_to_transaction, unbound_inscriptions, - utxo_cache, - txout_receiver, }; - if self.index.index_sats { - let mut sat_to_satpoint = wtx.open_table(SAT_TO_SATPOINT)?; - let mut outpoint_to_sat_ranges = wtx.open_table(OUTPOINT_TO_SAT_RANGES)?; - - let mut coinbase_inputs = VecDeque::new(); + let mut coinbase_inputs = VecDeque::new(); + if self.index.index_sats { let h = Height(self.height); if h.subsidy() > 0 { let start = h.starting_sat(); coinbase_inputs.push_front((start.n(), (start + h.subsidy()).n())); self.sat_ranges_since_flush += 1; } + } - for (tx_offset, (tx, txid)) in block.txdata.iter().enumerate().skip(1) { - log::trace!("Indexing transaction {tx_offset}…"); - - let mut input_sat_ranges = VecDeque::new(); + for (tx_offset, (tx, txid)) in block + .txdata + .iter() + .enumerate() + .skip(1) + .chain(block.txdata.iter().enumerate().take(1)) + { + log::trace!("Indexing transaction {tx_offset}…"); - for input in &tx.input { - let key = input.previous_output.store(); + let input_utxo_entries = if tx_offset == 0 { + Vec::new() + } else { + tx.input + .iter() + .map(|input| { + let outpoint = input.previous_output.store(); - let sat_ranges = match if self.index.index_spent_sats { - self.range_cache.get(&key).cloned() - } else { - self.range_cache.remove(&key) - } { - Some(sat_ranges) => { + let entry = if let Some(entry) = utxo_cache.remove(&OutPoint::load(outpoint)) { self.outputs_cached += 1; - sat_ranges - } - None => if self.index.index_spent_sats { - outpoint_to_sat_ranges.get(&key)? + entry + } else if let Some(entry) = outpoint_to_utxo_entry.remove(&outpoint)? { + if self.index.index_addresses { + let script_pubkey = entry.value().parse(self.index).script_pubkey(); + if !script_pubkey_to_outpoint.remove(script_pubkey, outpoint)? { + panic!("script pubkey entry ({script_pubkey:?}, {outpoint:?}) not found"); + } + } + + entry.value().to_buf() } else { - outpoint_to_sat_ranges.remove(&key)? - } - .ok_or_else(|| anyhow!("Could not find outpoint {} in index", input.previous_output))? - .value() - .to_vec(), - }; + assert!(!self.index.index_sats); + let txout = txout_receiver.blocking_recv().map_err(|err| { + anyhow!( + "failed to get transaction for {}: {err}", + input.previous_output + ) + })?; + + let mut entry = UtxoEntryBuf::new(); + entry.push_value(txout.value, self.index); + if self.index.index_addresses { + entry.push_script_pubkey(txout.script_pubkey.as_bytes(), self.index); + } - for chunk in sat_ranges.chunks_exact(11) { - input_sat_ranges.push_back(SatRange::load(chunk.try_into().unwrap())); + entry + }; + + Ok(entry) + }) + .collect::>>()? + }; + + let input_utxo_entries = input_utxo_entries + .iter() + .map(|entry| entry.parse(self.index)) + .collect::>(); + + let mut output_utxo_entries = tx + .output + .iter() + .map(|_| UtxoEntryBuf::new()) + .collect::>(); + + let mut orig_input_sat_ranges = None; + if self.index.index_sats { + let mut input_sat_ranges; + + if tx_offset == 0 { + // We use mem::take() because the borrow checker isn't smart enough + // to realize that coinbase_inputs won't be used again. + input_sat_ranges = mem::take(&mut coinbase_inputs); + } else { + input_sat_ranges = VecDeque::new(); + + for input_utxo_entry in &input_utxo_entries { + for chunk in input_utxo_entry.sat_ranges().chunks_exact(11) { + input_sat_ranges.push_back(SatRange::load(chunk.try_into().unwrap())); + } } } + orig_input_sat_ranges = Some(input_sat_ranges.clone()); + self.index_transaction_sats( tx, *txid, &mut sat_to_satpoint, + &mut output_utxo_entries, &mut input_sat_ranges, - &mut sat_ranges_written, - &mut outputs_in_block, - &mut inscription_updater, - index_inscriptions, + sat_ranges_written, + outputs_in_block, )?; - coinbase_inputs.extend(input_sat_ranges); + if tx_offset == 0 { + if !input_sat_ranges.is_empty() { + // Note that the lost-sats outpoint is special, because (unlike real + // outputs) it gets written to more than once. commit() will merge + // our new entry with any existing one. + let utxo_entry = utxo_cache + .entry(OutPoint::null()) + .or_insert(UtxoEntryBuf::empty(self.index)); + + let mut lost_sat_ranges = Vec::new(); + for (start, end) in input_sat_ranges { + if !Sat(start).common() { + sat_to_satpoint.insert( + &start, + &SatPoint { + outpoint: OutPoint::null(), + offset: lost_sats, + } + .store(), + )?; + } + + lost_sat_ranges.extend_from_slice(&(start, end).store()); + lost_sats += end - start; + } + + let mut new_utxo_entry = UtxoEntryBuf::new(); + new_utxo_entry.push_sat_ranges(&lost_sat_ranges, self.index); + if self.index.index_addresses { + new_utxo_entry.push_script_pubkey(&[], self.index); + } + + *utxo_entry = UtxoEntryBuf::merged(utxo_entry, &new_utxo_entry, self.index); + } + } else { + coinbase_inputs.extend(input_sat_ranges); + } + } else { + for (vout, txout) in tx.output.iter().enumerate() { + output_utxo_entries[vout].push_value(txout.value, self.index); + } } - if let Some((tx, txid)) = block.txdata.first() { - self.index_transaction_sats( + if self.index.index_addresses { + self.index_transaction_output_script_pubkeys(tx, &mut output_utxo_entries); + } + + if index_inscriptions { + inscription_updater.index_inscriptions( tx, *txid, - &mut sat_to_satpoint, - &mut coinbase_inputs, - &mut sat_ranges_written, - &mut outputs_in_block, - &mut inscription_updater, - index_inscriptions, + &input_utxo_entries, + &mut output_utxo_entries, + utxo_cache, + self.index, + orig_input_sat_ranges.as_ref(), )?; } - if !coinbase_inputs.is_empty() { - let mut lost_sat_ranges = outpoint_to_sat_ranges - .remove(&OutPoint::null().store())? - .map(|ranges| ranges.value().to_vec()) - .unwrap_or_default(); - - for (start, end) in coinbase_inputs { - if !Sat(start).common() { - sat_to_satpoint.insert( - &start, - &SatPoint { - outpoint: OutPoint::null(), - offset: lost_sats, - } - .store(), - )?; - } - - lost_sat_ranges.extend_from_slice(&(start, end).store()); - - lost_sats += end - start; - } - - outpoint_to_sat_ranges.insert(&OutPoint::null().store(), lost_sat_ranges.as_slice())?; - } - } else if index_inscriptions { - for (tx, txid) in block.txdata.iter().skip(1).chain(block.txdata.first()) { - inscription_updater.index_inscriptions(tx, *txid, None)?; + for (vout, output_utxo_entry) in output_utxo_entries.into_iter().enumerate() { + let vout = u32::try_from(vout).unwrap(); + utxo_cache.insert(OutPoint { txid: *txid, vout }, output_utxo_entry); } } @@ -623,108 +719,17 @@ impl<'index> Updater<'index> { &inscription_updater.unbound_inscriptions, )?; - if self.index.index_runes && self.height >= self.index.settings.first_rune_height() { - let mut outpoint_to_rune_balances = wtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?; - let mut rune_id_to_rune_entry = wtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; - let mut rune_to_rune_id = wtx.open_table(RUNE_TO_RUNE_ID)?; - let mut sequence_number_to_rune_id = wtx.open_table(SEQUENCE_NUMBER_TO_RUNE_ID)?; - let mut transaction_id_to_rune = wtx.open_table(TRANSACTION_ID_TO_RUNE)?; - - let runes = statistic_to_count - .get(&Statistic::Runes.into())? - .map(|x| x.value()) - .unwrap_or(0); - - let mut rune_updater = RuneUpdater { - event_sender: self.index.event_sender.as_ref(), - block_time: block.header.time, - burned: HashMap::new(), - client: &self.index.client, - height: self.height, - id_to_entry: &mut rune_id_to_rune_entry, - inscription_id_to_sequence_number: &mut inscription_id_to_sequence_number, - minimum: Rune::minimum_at_height( - self.index.settings.chain().network(), - Height(self.height), - ), - outpoint_to_balances: &mut outpoint_to_rune_balances, - rune_to_id: &mut rune_to_rune_id, - runes, - sequence_number_to_rune_id: &mut sequence_number_to_rune_id, - statistic_to_count: &mut statistic_to_count, - transaction_id_to_rune: &mut transaction_id_to_rune, - }; - - for (i, (tx, txid)) in block.txdata.iter().enumerate() { - rune_updater.index_runes(u32::try_from(i).unwrap(), tx, *txid)?; - } - - rune_updater.update()?; - } - - height_to_block_header.insert(&self.height, &block.header.store())?; - - self.height += 1; - self.outputs_traversed += outputs_in_block; - - log::info!( - "Wrote {sat_ranges_written} sat ranges from {outputs_in_block} outputs in {} ms", - (Instant::now() - start).as_millis(), - ); - Ok(()) } fn index_transaction_output_script_pubkeys( &mut self, tx: &Transaction, - txid: &Txid, - txout_receiver: &mut broadcast::Receiver, - utxo_cache: &mut HashMap, - script_pubkey_to_outpoint: &mut MultimapTable<&[u8], OutPointValue>, - outpoint_to_txout: &mut Table<&OutPointValue, TxOutValue>, - index_inscriptions: bool, - ) -> Result { - for txin in &tx.input { - let output = txin.previous_output; - if output.is_null() { - continue; - } - - // multi-level cache for UTXO set to get to the script pubkey - let txout = if let Some(txout) = utxo_cache.get(&txin.previous_output) { - txout.clone() - } else if let Some(value) = outpoint_to_txout.get(&txin.previous_output.store())? { - TxOut::load(value.value()) - } else { - txout_receiver.blocking_recv().map_err(|err| { - anyhow!( - "failed to get transaction for {}: {err}", - txin.previous_output.txid - ) - })? - }; - - // If we are indexing inscriptions, the InscriptionUpdater will remove these - if !index_inscriptions { - utxo_cache.remove(&output); - outpoint_to_txout.remove(&output.store())?; - } - - script_pubkey_to_outpoint.remove(&txout.script_pubkey.as_bytes(), output.store())?; - } - + output_utxo_entries: &mut [UtxoEntryBuf], + ) { for (vout, txout) in tx.output.iter().enumerate() { - let vout: u32 = vout.try_into().unwrap(); - script_pubkey_to_outpoint.insert( - txout.script_pubkey.as_bytes(), - OutPoint { txid: *txid, vout }.store(), - )?; - - utxo_cache.insert(OutPoint { txid: *txid, vout }, txout.clone()); + output_utxo_entries[vout].push_script_pubkey(txout.script_pubkey.as_bytes(), self.index); } - - Ok(()) } fn index_transaction_sats( @@ -732,16 +737,11 @@ impl<'index> Updater<'index> { tx: &Transaction, txid: Txid, sat_to_satpoint: &mut Table, + output_utxo_entries: &mut [UtxoEntryBuf], input_sat_ranges: &mut VecDeque<(u64, u64)>, sat_ranges_written: &mut u64, outputs_traversed: &mut u64, - inscription_updater: &mut InscriptionUpdater, - index_inscriptions: bool, ) -> Result { - if index_inscriptions { - inscription_updater.index_inscriptions(tx, txid, Some(input_sat_ranges))?; - } - for (vout, output) in tx.output.iter().enumerate() { let outpoint = OutPoint { vout: vout.try_into().unwrap(), @@ -786,46 +786,51 @@ impl<'index> Updater<'index> { *outputs_traversed += 1; - self.range_cache.insert(outpoint.store(), sats); - self.outputs_inserted_since_flush += 1; + output_utxo_entries[vout].push_sat_ranges(&sats, self.index); } Ok(()) } - fn commit(&mut self, wtx: WriteTransaction, utxo_cache: HashMap) -> Result { + fn commit( + &mut self, + wtx: WriteTransaction, + utxo_cache: HashMap, + ) -> Result { log::info!( "Committing at block height {}, {} outputs traversed, {} in map, {} cached", self.height, self.outputs_traversed, - self.range_cache.len(), + utxo_cache.len(), self.outputs_cached ); - if self.index.index_sats { - log::info!( - "Flushing {} entries ({:.1}% resulting from {} insertions) from memory to database", - self.range_cache.len(), - self.range_cache.len() as f64 / self.outputs_inserted_since_flush as f64 * 100., - self.outputs_inserted_since_flush, - ); - - let mut outpoint_to_sat_ranges = wtx.open_table(OUTPOINT_TO_SAT_RANGES)?; - - for (outpoint, sat_ranges) in self.range_cache.drain() { - outpoint_to_sat_ranges.insert(&outpoint, sat_ranges.as_slice())?; - } + { + let mut outpoint_to_utxo_entry = wtx.open_table(OUTPOINT_TO_UTXO_ENTRY)?; + let mut script_pubkey_to_outpoint = wtx.open_multimap_table(SCRIPT_PUBKEY_TO_OUTPOINT)?; + let mut sequence_number_to_satpoint = wtx.open_table(SEQUENCE_NUMBER_TO_SATPOINT)?; - self.outputs_inserted_since_flush = 0; - } + for (outpoint, mut utxo_entry) in utxo_cache { + if Index::is_special_outpoint(outpoint) { + if let Some(old_entry) = outpoint_to_utxo_entry.get(&outpoint.store())? { + utxo_entry = UtxoEntryBuf::merged(old_entry.value(), &utxo_entry, self.index); + } + } - { - log::info!("Flushing utxo cache with {} entries", utxo_cache.len()); + outpoint_to_utxo_entry.insert(&outpoint.store(), utxo_entry.as_ref())?; - let mut outpoint_to_txout = wtx.open_table(OUTPOINT_TO_TXOUT)?; + let utxo_entry = utxo_entry.parse(self.index); + if self.index.index_addresses { + let script_pubkey = utxo_entry.script_pubkey(); + script_pubkey_to_outpoint.insert(script_pubkey, &outpoint.store())?; + } - for (outpoint, txout) in utxo_cache { - outpoint_to_txout.insert(&outpoint.store(), txout.store())?; + if self.index.index_inscriptions { + for (sequence_number, offset) in utxo_entry.parse_inscriptions() { + let satpoint = SatPoint { outpoint, offset }; + sequence_number_to_satpoint.insert(sequence_number, &satpoint.store())?; + } + } } } diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 218c80e0c1..feaabbd619 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -27,44 +27,35 @@ enum Origin { fee: u64, hidden: bool, parents: Vec, - pointer: Option, reinscription: bool, unbound: bool, vindicated: bool, }, Old { + sequence_number: u32, old_satpoint: SatPoint, }, } pub(super) struct InscriptionUpdater<'a, 'tx> { pub(super) blessed_inscription_count: u64, - pub(super) chain: Chain, pub(super) cursed_inscription_count: u64, - pub(super) event_sender: Option<&'a mpsc::Sender>, pub(super) flotsam: Vec, pub(super) height: u32, pub(super) home_inscription_count: u64, pub(super) home_inscriptions: &'a mut Table<'tx, u32, InscriptionIdValue>, pub(super) id_to_sequence_number: &'a mut Table<'tx, InscriptionIdValue, u32>, - pub(super) index_addresses: bool, - pub(super) index_transactions: bool, pub(super) inscription_number_to_sequence_number: &'a mut Table<'tx, i32, u32>, pub(super) lost_sats: u64, pub(super) next_sequence_number: u32, - pub(super) outpoint_to_txout: &'a mut Table<'tx, &'static OutPointValue, TxOutValue>, pub(super) reward: u64, pub(super) transaction_buffer: Vec, pub(super) transaction_id_to_transaction: &'a mut Table<'tx, &'static TxidValue, &'static [u8]>, pub(super) sat_to_sequence_number: &'a mut MultimapTable<'tx, u64, u32>, - pub(super) satpoint_to_sequence_number: &'a mut MultimapTable<'tx, &'static SatPointValue, u32>, pub(super) sequence_number_to_children: &'a mut MultimapTable<'tx, u32, u32>, pub(super) sequence_number_to_entry: &'a mut Table<'tx, u32, InscriptionEntryValue>, - pub(super) sequence_number_to_satpoint: &'a mut Table<'tx, u32, &'static SatPointValue>, pub(super) timestamp: u32, pub(super) unbound_inscriptions: u64, - pub(super) utxo_cache: &'a mut HashMap, - pub(super) txout_receiver: &'a mut broadcast::Receiver, } impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { @@ -72,17 +63,21 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { &mut self, tx: &Transaction, txid: Txid, + input_utxo_entries: &[ParsedUtxoEntry], + output_utxo_entries: &mut [UtxoEntryBuf], + utxo_cache: &mut HashMap, + index: &Index, input_sat_ranges: Option<&VecDeque<(u64, u64)>>, ) -> Result { let mut floating_inscriptions = Vec::new(); let mut id_counter = 0; let mut inscribed_offsets = BTreeMap::new(); - let jubilant = self.height >= self.chain.jubilee_height(); + let jubilant = self.height >= index.settings.chain().jubilee_height(); let mut total_input_value = 0; let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); let envelopes = ParsedEnvelope::from_transaction(tx); - let inscriptions = !envelopes.is_empty(); + let has_new_inscriptions = !envelopes.is_empty(); let mut envelopes = envelopes.into_iter().peekable(); for (input_index, txin) in tx.input.iter().enumerate() { @@ -92,17 +87,33 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { continue; } - // find existing inscriptions on input (transfers of inscriptions) - for (old_satpoint, inscription_id) in Index::inscriptions_on_output( - self.satpoint_to_sequence_number, - self.sequence_number_to_entry, - txin.previous_output, - )? { - let offset = total_input_value + old_satpoint.offset; + let mut transferred_inscriptions = input_utxo_entries[input_index].parse_inscriptions(); + + transferred_inscriptions.sort_by_key(|(sequence_number, _)| *sequence_number); + + for (sequence_number, old_satpoint_offset) in transferred_inscriptions { + let old_satpoint = SatPoint { + outpoint: txin.previous_output, + offset: old_satpoint_offset, + }; + + let inscription_id = InscriptionEntry::load( + self + .sequence_number_to_entry + .get(sequence_number)? + .unwrap() + .value(), + ) + .id; + + let offset = total_input_value + old_satpoint_offset; floating_inscriptions.push(Flotsam { offset, inscription_id, - origin: Origin::Old { old_satpoint }, + origin: Origin::Old { + sequence_number, + old_satpoint, + }, }); inscribed_offsets @@ -113,24 +124,8 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { let offset = total_input_value; - // multi-level cache for UTXO set to get to the input amount - let txout = if let Some(txout) = self.utxo_cache.remove(&txin.previous_output) { - txout - } else if let Some(value) = self - .outpoint_to_txout - .remove(&txin.previous_output.store())? - { - TxOut::load(value.value()) - } else { - self.txout_receiver.blocking_recv().map_err(|err| { - anyhow!( - "failed to get transaction for {}: {err}", - txin.previous_output.txid - ) - })? - }; - - total_input_value += txout.value; + let input_value = input_utxo_entries[input_index].total_value(); + total_input_value += input_value; // go through all inscriptions in this input while let Some(inscription) = envelopes.peek() { @@ -201,9 +196,8 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { fee: 0, hidden: inscription.payload.hidden(), parents: inscription.payload.parents(), - pointer: inscription.payload.pointer(), reinscription: inscribed_offsets.contains_key(&offset), - unbound: txout.value == 0 + unbound: input_value == 0 || curse == Some(Curse::UnrecognizedEvenField) || inscription.payload.unrecognized_even_field, vindicated: curse.is_some() && jubilant, @@ -220,7 +214,7 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { } } - if self.index_transactions && inscriptions { + if index.index_transactions && has_new_inscriptions { tx.consensus_encode(&mut self.transaction_buffer) .expect("in-memory writers don't error"); @@ -275,7 +269,6 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { floating_inscriptions.sort_by_key(|flotsam| flotsam.offset); let mut inscriptions = floating_inscriptions.into_iter().peekable(); - let mut range_to_vout = BTreeMap::new(); let mut new_locations = Vec::new(); let mut output_value = 0; for (vout, txout) in tx.output.iter().enumerate() { @@ -301,44 +294,22 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { )); } - range_to_vout.insert((output_value, end), vout.try_into().unwrap()); - output_value = end; - - if !self.index_addresses { - self.utxo_cache.insert( - OutPoint { - vout: vout.try_into().unwrap(), - txid, - }, - txout.clone(), - ); - } } - for (new_satpoint, mut flotsam, op_return) in new_locations.into_iter() { - let new_satpoint = match flotsam.origin { - Origin::New { - pointer: Some(pointer), - .. - } if pointer < output_value => { - match range_to_vout.iter().find_map(|((start, end), vout)| { - (pointer >= *start && pointer < *end).then(|| (vout, pointer - start)) - }) { - Some((vout, offset)) => { - flotsam.offset = pointer; - SatPoint { - outpoint: OutPoint { txid, vout: *vout }, - offset, - } - } - _ => new_satpoint, - } - } - _ => new_satpoint, - }; - - self.update_inscription_location(input_sat_ranges, flotsam, new_satpoint, op_return)?; + for (new_satpoint, flotsam, op_return) in new_locations.into_iter() { + let output_utxo_entry = + &mut output_utxo_entries[usize::try_from(new_satpoint.outpoint.vout).unwrap()]; + + self.update_inscription_location( + input_sat_ranges, + flotsam, + new_satpoint, + op_return, + Some(output_utxo_entry), + utxo_cache, + index, + )?; } if is_coinbase { @@ -347,7 +318,15 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { outpoint: OutPoint::null(), offset: self.lost_sats + flotsam.offset - output_value, }; - self.update_inscription_location(input_sat_ranges, flotsam, new_satpoint, false)?; + self.update_inscription_location( + input_sat_ranges, + flotsam, + new_satpoint, + false, + None, + utxo_cache, + index, + )?; } self.lost_sats += self.reward - output_value; Ok(()) @@ -386,20 +365,16 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { flotsam: Flotsam, new_satpoint: SatPoint, op_return: bool, + mut normal_output_utxo_entry: Option<&mut UtxoEntryBuf>, + utxo_cache: &mut HashMap, + index: &Index, ) -> Result { let inscription_id = flotsam.inscription_id; let (unbound, sequence_number) = match flotsam.origin { - Origin::Old { old_satpoint } => { - self - .satpoint_to_sequence_number - .remove_all(&old_satpoint.store())?; - - let sequence_number = self - .id_to_sequence_number - .get(&inscription_id.store())? - .unwrap() - .value(); - + Origin::Old { + sequence_number, + old_satpoint, + } => { if op_return { let entry = InscriptionEntry::load( self @@ -418,7 +393,7 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { )?; } - if let Some(sender) = self.event_sender { + if let Some(ref sender) = index.event_sender { sender.blocking_send(Event::InscriptionTransferred { block_height: self.height, inscription_id, @@ -435,7 +410,6 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { fee, hidden, parents, - pointer: _, reinscription, unbound, vindicated, @@ -514,7 +488,7 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { }) .collect::>>()?; - if let Some(sender) = self.event_sender { + if let Some(ref sender) = index.event_sender { sender.blocking_send(Event::InscriptionCreated { block_height: self.height, charms, @@ -567,17 +541,24 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { offset: self.unbound_inscriptions, }; self.unbound_inscriptions += 1; - new_unbound_satpoint.store() + normal_output_utxo_entry = None; + new_unbound_satpoint } else { - new_satpoint.store() + new_satpoint }; - self - .satpoint_to_sequence_number - .insert(&satpoint, sequence_number)?; - self - .sequence_number_to_satpoint - .insert(sequence_number, &satpoint)?; + // The special outpoints, i.e., the null outpoint and the unbound outpoint, + // don't follow the normal rulesr. Unlike real outputs they get written to + // more than once. So we create a new UTXO entry here and commit() will + // merge it with any existing entry. + let output_utxo_entry = normal_output_utxo_entry.unwrap_or_else(|| { + assert!(Index::is_special_outpoint(satpoint.outpoint)); + utxo_cache + .entry(satpoint.outpoint) + .or_insert(UtxoEntryBuf::empty(index)) + }); + + output_utxo_entry.push_inscription(sequence_number, satpoint.offset, index); Ok(()) } diff --git a/src/index/utxo_entry.rs b/src/index/utxo_entry.rs new file mode 100644 index 0000000000..40eadbc59f --- /dev/null +++ b/src/index/utxo_entry.rs @@ -0,0 +1,323 @@ +use { + super::{ + entry::{Entry, SatRange}, + Index, + }, + ordinals::varint, + redb::TypeName, + ref_cast::RefCast, + std::ops::Deref, +}; + +enum Sats<'a> { + Ranges(&'a [u8]), + Value(u64), +} + +/// A `UtxoEntry` stores the following information about an unspent transaction +/// output, depending on the indexing options: +/// +/// If `--index-sats`, the full list of sat ranges, stored as a varint followed +/// by that many 11-byte sat range entries, otherwise the total output value +/// stored as a varint. +/// +/// If `--index-addresses`, the script pubkey stored as a varint followed by +/// that many bytes of data. +/// +/// If `--index-inscriptions`, the list of inscriptions stored as +/// `(sequence_number, offset)`, with the sequence number stored as a u32 and +/// the offset as a varint. +/// +/// Note that the list of inscriptions doesn't need an explicit length, it +/// continues until the end of the array. +/// +/// A `UtxoEntry` is the read-only value stored in redb as a byte string. A +/// `UtxoEntryBuf` is the writeable version, used for constructing new +/// `UtxoEntry`s. A `ParsedUtxoEntry` is the parsed value. +#[derive(Debug, RefCast)] +#[repr(transparent)] +pub struct UtxoEntry { + bytes: [u8], +} + +impl UtxoEntry { + pub fn parse(&self, index: &Index) -> ParsedUtxoEntry { + let sats; + let mut script_pubkey = None; + let mut inscriptions = None; + + let mut offset = 0; + if index.index_sats { + let (num_sat_ranges, varint_len) = varint::decode(&self.bytes).unwrap(); + offset += varint_len; + + let num_sat_ranges: usize = num_sat_ranges.try_into().unwrap(); + let sat_ranges_len = num_sat_ranges * 11; + sats = Sats::Ranges(&self.bytes[offset..offset + sat_ranges_len]); + offset += sat_ranges_len; + } else { + let (value, varint_len) = varint::decode(&self.bytes).unwrap(); + sats = Sats::Value(value.try_into().unwrap()); + offset += varint_len; + }; + + if index.index_addresses { + let (script_pubkey_len, varint_len) = varint::decode(&self.bytes[offset..]).unwrap(); + offset += varint_len; + + let script_pubkey_len: usize = script_pubkey_len.try_into().unwrap(); + script_pubkey = Some(&self.bytes[offset..offset + script_pubkey_len]); + offset += script_pubkey_len; + } + + if index.index_inscriptions { + inscriptions = Some(&self.bytes[offset..self.bytes.len()]); + } + + ParsedUtxoEntry { + sats, + script_pubkey, + inscriptions, + } + } + + pub fn to_buf(&self) -> UtxoEntryBuf { + UtxoEntryBuf { + vec: self.bytes.to_vec(), + #[cfg(debug_assertions)] + state: State::Valid, + } + } +} + +impl redb::Value for &UtxoEntry { + type SelfType<'a> = &'a UtxoEntry where Self: 'a; + type AsBytes<'a> = &'a [u8] where Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + UtxoEntry::ref_cast(data) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + &value.bytes + } + + fn type_name() -> TypeName { + TypeName::new("&ord::index::utxo_entry::UtxoValue") + } +} + +pub struct ParsedUtxoEntry<'a> { + sats: Sats<'a>, + script_pubkey: Option<&'a [u8]>, + inscriptions: Option<&'a [u8]>, +} + +impl<'a> ParsedUtxoEntry<'a> { + pub fn total_value(&self) -> u64 { + match self.sats { + Sats::Value(value) => value, + Sats::Ranges(ranges) => { + let mut value = 0; + for chunk in ranges.chunks_exact(11) { + let range = SatRange::load(chunk.try_into().unwrap()); + value += range.1 - range.0; + } + value + } + } + } + + pub fn sat_ranges(&self) -> &'a [u8] { + let Sats::Ranges(ranges) = self.sats else { + panic!("sat ranges are missing"); + }; + ranges + } + + pub fn script_pubkey(&self) -> &'a [u8] { + self.script_pubkey.unwrap() + } + + pub fn inscriptions(&self) -> &'a [u8] { + self.inscriptions.unwrap() + } + + pub fn parse_inscriptions(&self) -> Vec<(u32, u64)> { + let inscriptions = self.inscriptions.unwrap(); + let mut byte_offset = 0; + let mut parsed_inscriptions = Vec::new(); + + while byte_offset < inscriptions.len() { + let sequence_number = u32::from_le_bytes( + inscriptions[byte_offset..byte_offset + 4] + .try_into() + .unwrap(), + ); + byte_offset += 4; + + let (satpoint_offset, varint_len) = varint::decode(&inscriptions[byte_offset..]).unwrap(); + let satpoint_offset = u64::try_from(satpoint_offset).unwrap(); + byte_offset += varint_len; + + parsed_inscriptions.push((sequence_number, satpoint_offset)); + } + + parsed_inscriptions + } +} + +#[cfg(debug_assertions)] +#[derive(Debug, Eq, PartialEq)] +enum State { + NeedSats, + NeedScriptPubkey, + Valid, +} + +#[derive(Debug)] +pub struct UtxoEntryBuf { + vec: Vec, + #[cfg(debug_assertions)] + state: State, +} + +impl UtxoEntryBuf { + pub fn new() -> Self { + Self { + vec: Vec::new(), + #[cfg(debug_assertions)] + state: State::NeedSats, + } + } + + pub fn push_value(&mut self, value: u64, index: &Index) { + assert!(!index.index_sats); + varint::encode_to_vec(value.into(), &mut self.vec); + + #[cfg(debug_assertions)] + self.advance_state(State::NeedSats, State::NeedScriptPubkey, index); + } + + pub fn push_sat_ranges(&mut self, sat_ranges: &[u8], index: &Index) { + assert!(index.index_sats); + let num_sat_ranges = sat_ranges.len() / 11; + assert!(num_sat_ranges * 11 == sat_ranges.len()); + varint::encode_to_vec(num_sat_ranges.try_into().unwrap(), &mut self.vec); + self.vec.extend(sat_ranges); + + #[cfg(debug_assertions)] + self.advance_state(State::NeedSats, State::NeedScriptPubkey, index); + } + + pub fn push_script_pubkey(&mut self, script_pubkey: &[u8], index: &Index) { + assert!(index.index_addresses); + varint::encode_to_vec(script_pubkey.len().try_into().unwrap(), &mut self.vec); + self.vec.extend(script_pubkey); + + #[cfg(debug_assertions)] + self.advance_state(State::NeedScriptPubkey, State::Valid, index); + } + + pub fn push_inscriptions(&mut self, inscriptions: &[u8], index: &Index) { + assert!(index.index_inscriptions); + self.vec.extend(inscriptions); + + #[cfg(debug_assertions)] + self.advance_state(State::Valid, State::Valid, index); + } + + pub fn push_inscription(&mut self, sequence_number: u32, satpoint_offset: u64, index: &Index) { + assert!(index.index_inscriptions); + self.vec.extend(sequence_number.to_le_bytes()); + varint::encode_to_vec(satpoint_offset.into(), &mut self.vec); + + #[cfg(debug_assertions)] + self.advance_state(State::Valid, State::Valid, index); + } + + #[cfg(debug_assertions)] + fn advance_state(&mut self, expected_state: State, new_state: State, index: &Index) { + assert!(self.state == expected_state); + self.state = new_state; + + if self.state == State::NeedScriptPubkey && !index.index_addresses { + self.state = State::Valid; + } + } + + pub fn merged(a: &UtxoEntry, b: &UtxoEntry, index: &Index) -> Self { + let a_parsed = a.parse(index); + let b_parsed = b.parse(index); + let mut merged = Self::new(); + + if index.index_sats { + let sat_ranges = [a_parsed.sat_ranges(), b_parsed.sat_ranges()].concat(); + merged.push_sat_ranges(&sat_ranges, index); + } else { + assert!(a_parsed.total_value() == 0); + assert!(b_parsed.total_value() == 0); + merged.push_value(0, index); + } + + if index.index_addresses { + assert!(a_parsed.script_pubkey().is_empty()); + assert!(b_parsed.script_pubkey().is_empty()); + merged.push_script_pubkey(&[], index); + } + + if index.index_inscriptions { + merged.push_inscriptions(a_parsed.inscriptions(), index); + merged.push_inscriptions(b_parsed.inscriptions(), index); + } + + merged + } + + pub fn empty(index: &Index) -> Self { + let mut utxo_entry = Self::new(); + + if index.index_sats { + utxo_entry.push_sat_ranges(&[], index); + } else { + utxo_entry.push_value(0, index); + } + + if index.index_addresses { + utxo_entry.push_script_pubkey(&[], index); + } + + utxo_entry + } + + pub fn as_ref(&self) -> &UtxoEntry { + #[cfg(debug_assertions)] + assert!(self.state == State::Valid); + UtxoEntry::ref_cast(&self.vec) + } +} + +impl Default for UtxoEntryBuf { + fn default() -> Self { + Self::new() + } +} + +impl Deref for UtxoEntryBuf { + type Target = UtxoEntry; + + fn deref(&self) -> &UtxoEntry { + self.as_ref() + } +} diff --git a/src/lib.rs b/src/lib.rs index e172e7da8c..00e965c600 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -246,6 +246,7 @@ fn gracefully_shut_down_indexer() { pub fn main() { env_logger::init(); + ctrlc::set_handler(move || { if SHUTTING_DOWN.fetch_or(true, atomic::Ordering::Relaxed) { process::exit(1); diff --git a/src/options.rs b/src/options.rs index 051712a364..2a53db7f93 100644 --- a/src/options.rs +++ b/src/options.rs @@ -61,8 +61,6 @@ pub struct Options { pub(crate) index_runes: bool, #[arg(long, help = "Track location of all satoshis.")] pub(crate) index_sats: bool, - #[arg(long, help = "Keep sat index entries of spent outputs.")] - pub(crate) index_spent_sats: bool, #[arg(long, help = "Store transactions in index.")] pub(crate) index_transactions: bool, #[arg(long, help = "Run in integration test mode.")] diff --git a/src/settings.rs b/src/settings.rs index 05e587a1f2..3482286e00 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -23,7 +23,6 @@ pub struct Settings { index_cache_size: Option, index_runes: bool, index_sats: bool, - index_spent_sats: bool, index_transactions: bool, integration_test: bool, no_index_inscriptions: bool, @@ -140,7 +139,6 @@ impl Settings { index_cache_size: self.index_cache_size.or(source.index_cache_size), index_runes: self.index_runes || source.index_runes, index_sats: self.index_sats || source.index_sats, - index_spent_sats: self.index_spent_sats || source.index_spent_sats, index_transactions: self.index_transactions || source.index_transactions, integration_test: self.integration_test || source.integration_test, no_index_inscriptions: self.no_index_inscriptions || source.no_index_inscriptions, @@ -177,7 +175,6 @@ impl Settings { index_cache_size: options.index_cache_size, index_runes: options.index_runes, index_sats: options.index_sats, - index_spent_sats: options.index_spent_sats, index_transactions: options.index_transactions, integration_test: options.integration_test, no_index_inscriptions: options.no_index_inscriptions, @@ -267,7 +264,6 @@ impl Settings { index_cache_size: get_usize("INDEX_CACHE_SIZE")?, index_runes: get_bool("INDEX_RUNES"), index_sats: get_bool("INDEX_SATS"), - index_spent_sats: get_bool("INDEX_SPENT_SATS"), index_transactions: get_bool("INDEX_TRANSACTIONS"), integration_test: get_bool("INTEGRATION_TEST"), no_index_inscriptions: get_bool("NO_INDEX_INSCRIPTIONS"), @@ -299,7 +295,6 @@ impl Settings { index_cache_size: None, index_runes: true, index_sats: true, - index_spent_sats: false, index_transactions: false, integration_test: false, no_index_inscriptions: false, @@ -381,7 +376,6 @@ impl Settings { }), index_runes: self.index_runes, index_sats: self.index_sats, - index_spent_sats: self.index_spent_sats, index_transactions: self.index_transactions, integration_test: self.integration_test, no_index_inscriptions: self.no_index_inscriptions, @@ -553,10 +547,6 @@ impl Settings { self.index_sats } - pub fn index_spent_sats_raw(&self) -> bool { - self.index_spent_sats - } - pub fn index_transactions_raw(&self) -> bool { self.index_transactions } @@ -1032,7 +1022,6 @@ mod tests { ("INDEX_ADDRESSES", "1"), ("INDEX_RUNES", "1"), ("INDEX_SATS", "1"), - ("INDEX_SPENT_SATS", "1"), ("INDEX_TRANSACTIONS", "1"), ("INTEGRATION_TEST", "1"), ("NO_INDEX_INSCRIPTIONS", "1"), @@ -1078,7 +1067,6 @@ mod tests { index_cache_size: Some(4), index_runes: true, index_sats: true, - index_spent_sats: true, index_transactions: true, integration_test: true, no_index_inscriptions: true, @@ -1112,7 +1100,6 @@ mod tests { "--index-cache-size=4", "--index-runes", "--index-sats", - "--index-spent-sats", "--index-transactions", "--index=index", "--integration-test", @@ -1143,7 +1130,6 @@ mod tests { index_cache_size: Some(4), index_runes: true, index_sats: true, - index_spent_sats: true, index_transactions: true, integration_test: true, no_index_inscriptions: true, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index a01a767f0c..f233e3a1d6 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -3742,27 +3742,6 @@ mod tests { ); } - #[test] - fn null_output_is_initially_empty() { - let txid = "0000000000000000000000000000000000000000000000000000000000000000"; - TestServer::builder().index_sats().build().assert_response_regex( - format!("/output/{txid}:4294967295"), - StatusCode::OK, - format!( - ".*Output {txid}:4294967295.*

Output {txid}:4294967295

-
-
value
0
-
script pubkey
-
transaction
{txid}
-
spent
false
-
-

0 Sat Ranges

-
    -
.*" - ), - ); - } - #[test] fn null_output_receives_lost_sats() { let server = TestServer::builder().index_sats().build(); diff --git a/tests/info.rs b/tests/info.rs index 170ec5aab8..7c294f9933 100644 --- a/tests/info.rs +++ b/tests/info.rs @@ -27,7 +27,7 @@ fn json_with_satoshi_index() { \} \], "tree_height": \d+, - "utxos_indexed": 2 + "utxos_indexed": 1 \} "#, ) @@ -61,7 +61,7 @@ fn json_without_satoshi_index() { \} \], "tree_height": \d+, - "utxos_indexed": 0 + "utxos_indexed": 1 \} "#, ) diff --git a/tests/settings.rs b/tests/settings.rs index d5d310e9ea..08738ca11d 100644 --- a/tests/settings.rs +++ b/tests/settings.rs @@ -26,7 +26,6 @@ fn default() { "index_cache_size": \d+, "index_runes": false, "index_sats": false, - "index_spent_sats": false, "index_transactions": false, "integration_test": false, "no_index_inscriptions": false,