diff --git a/src/index.rs b/src/index.rs index db1bfe6005..5190bba3ce 100644 --- a/src/index.rs +++ b/src/index.rs @@ -94,6 +94,7 @@ pub(crate) enum Statistic { OutputsTraversed = 0, Commits = 1, SatRanges = 2, + LostSats = 3, } impl Statistic { @@ -247,7 +248,7 @@ impl Index { }) } - pub(crate) fn has_satoshi_index(&self) -> Result { + pub(crate) fn has_sat_index(&self) -> Result { match self.begin_read()?.0.open_table(OUTPOINT_TO_SAT_RANGES) { Ok(_) => Ok(true), Err(redb::Error::TableDoesNotExist(_)) => Ok(false), @@ -256,7 +257,7 @@ impl Index { } fn require_satoshi_index(&self, feature: &str) -> Result { - if !self.has_satoshi_index()? { + if !self.has_sat_index()? { bail!("{feature} requires index created with `--index-sats` flag") } @@ -364,16 +365,17 @@ impl Index { } #[cfg(test)] - pub(crate) fn statistic(&self, statistic: Statistic) -> Result { - Ok( - self - .database - .begin_read()? - .open_table(STATISTIC_TO_COUNT)? - .get(&statistic.key())? - .map(|x| x.value()) - .unwrap_or(0), - ) + pub(crate) fn statistic(&self, statistic: Statistic) -> u64 { + self + .database + .begin_read() + .unwrap() + .open_table(STATISTIC_TO_COUNT) + .unwrap() + .get(&statistic.key()) + .unwrap() + .map(|x| x.value()) + .unwrap_or(0) } pub(crate) fn height(&self) -> Result> { @@ -401,7 +403,7 @@ impl Index { } pub(crate) fn rare_sat_satpoints(&self) -> Result>> { - if self.has_satoshi_index()? { + if self.has_sat_index()? { let mut result = Vec::new(); let rtx = self.database.begin_read()?; @@ -419,7 +421,7 @@ impl Index { } pub(crate) fn rare_sat_satpoint(&self, sat: Sat) -> Result> { - if self.has_satoshi_index()? { + if self.has_sat_index()? { Ok( self .database @@ -671,7 +673,12 @@ impl Index { } #[cfg(test)] - fn assert_inscription_location(&self, inscription_id: InscriptionId, satpoint: SatPoint) { + fn assert_inscription_location( + &self, + inscription_id: InscriptionId, + satpoint: SatPoint, + sat: u64, + ) { let rtx = self.database.begin_read().unwrap(); let satpoint_to_inscription_id = rtx.open_table(SATPOINT_TO_INSCRIPTION_ID).unwrap(); @@ -704,6 +711,34 @@ impl Index { ), inscription_id, ); + + if self.has_sat_index().unwrap() { + assert_eq!( + InscriptionId::from_inner( + *rtx + .open_table(SAT_TO_INSCRIPTION_ID) + .unwrap() + .get(&sat) + .unwrap() + .unwrap() + .value() + ), + inscription_id, + ); + + assert_eq!( + decode_satpoint( + *rtx + .open_table(SAT_TO_SATPOINT) + .unwrap() + .get(&sat) + .unwrap() + .unwrap() + .value() + ), + satpoint, + ); + } } } @@ -719,10 +754,6 @@ mod tests { } impl Context { - fn new() -> Self { - Self::with_args("") - } - fn with_args(args: &str) -> Self { let rpc_server = test_bitcoincore_rpc::spawn(); @@ -756,11 +787,21 @@ mod tests { } } - fn mine_blocks(&self, num: u64) -> Vec { - let blocks = self.rpc_server.mine_blocks(num); + fn mine_blocks(&self, n: u64) -> Vec { + let blocks = self.rpc_server.mine_blocks(n); self.index.update().unwrap(); blocks } + + fn mine_blocks_with_subsidy(&self, n: u64, subsidy: u64) -> Vec { + let blocks = self.rpc_server.mine_blocks_with_subsidy(n, subsidy); + self.index.update().unwrap(); + blocks + } + + fn configurations() -> Vec { + vec![Context::with_args(""), Context::with_args("--index-sats")] + } } #[test] @@ -820,8 +861,8 @@ mod tests { context.mine_blocks(1); let split_coinbase_output = TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 2, + inputs: &[(1, 0, 0)], + outputs: 2, fee: 0, ..Default::default() }; @@ -846,8 +887,7 @@ mod tests { context.mine_blocks(2); let merge_coinbase_outputs = TransactionTemplate { - input_slots: &[(1, 0, 0), (2, 0, 0)], - output_count: 1, + inputs: &[(1, 0, 0), (2, 0, 0)], fee: 0, ..Default::default() }; @@ -870,8 +910,8 @@ mod tests { context.mine_blocks(1); let fee_paying_tx = TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 2, + inputs: &[(1, 0, 0)], + outputs: 2, fee: 10, ..Default::default() }; @@ -904,14 +944,12 @@ mod tests { context.mine_blocks(2); let first_fee_paying_tx = TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 1, + inputs: &[(1, 0, 0)], fee: 10, ..Default::default() }; let second_fee_paying_tx = TransactionTemplate { - input_slots: &[(2, 0, 0)], - output_count: 1, + inputs: &[(2, 0, 0)], fee: 10, ..Default::default() }; @@ -940,8 +978,7 @@ mod tests { context.mine_blocks(1); let no_value_output = TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 1, + inputs: &[(1, 0, 0)], fee: 50 * COIN_VALUE, ..Default::default() }; @@ -960,8 +997,7 @@ mod tests { context.mine_blocks(1); let no_value_output = TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 1, + inputs: &[(1, 0, 0)], fee: 50 * COIN_VALUE, ..Default::default() }; @@ -969,8 +1005,7 @@ mod tests { context.mine_blocks(1); let no_value_input = TransactionTemplate { - input_slots: &[(2, 1, 0)], - output_count: 1, + inputs: &[(2, 1, 0)], fee: 0, ..Default::default() }; @@ -988,8 +1023,7 @@ mod tests { let context = Context::with_args("--index-sats"); context.mine_blocks(1); context.rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 1, + inputs: &[(1, 0, 0)], fee: 0, ..Default::default() }); @@ -1072,8 +1106,7 @@ mod tests { let context = Context::with_args("--index-sats"); context.mine_blocks(1); let spend_txid = context.rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 1, + inputs: &[(1, 0, 0)], fee: 0, ..Default::default() }); @@ -1087,193 +1120,497 @@ mod tests { ) } + #[test] + fn inscriptions_are_tracked_correctly() { + for context in Context::configurations() { + context.mine_blocks(1); + + let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); + + context.mine_blocks(1); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { + txid: inscription_id, + vout: 0, + }, + offset: 0, + }, + 50 * COIN_VALUE, + ); + } + } + #[test] fn unaligned_inscriptions_are_tracked_correctly() { - let context = Context::new(); - context.mine_blocks(1); + for context in Context::configurations() { + context.mine_blocks(1); - let inscription = inscription("text/plain", "hello"); - let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 1, - fee: 0, - witness: inscription.to_witness(), - }); + let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); - context.mine_blocks(1); + context.mine_blocks(1); - context.index.assert_inscription_location( - inscription_id, - SatPoint { - outpoint: OutPoint { - txid: inscription_id, - vout: 0, + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { + txid: inscription_id, + vout: 0, + }, + offset: 0, }, - offset: 0, - }, - ); + 50 * COIN_VALUE, + ); - let send_txid = context.rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(2, 0, 0), (2, 1, 0)], - output_count: 1, - ..Default::default() - }); + let send_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0), (2, 1, 0)], + ..Default::default() + }); - context.mine_blocks(1); + context.mine_blocks(1); - context.index.assert_inscription_location( - inscription_id, - SatPoint { - outpoint: OutPoint { - txid: send_txid, - vout: 0, + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { + txid: send_txid, + vout: 0, + }, + offset: 50 * COIN_VALUE, }, - offset: 50 * COIN_VALUE, - }, - ); + 50 * COIN_VALUE, + ); + } } #[test] fn merged_inscriptions_are_tracked_correctly() { - let context = Context::new(); - context.mine_blocks(2); + for context in Context::configurations() { + context.mine_blocks(2); - let first_inscription = inscription("text/plain", "hello"); - let first_inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 1, - fee: 0, - witness: first_inscription.to_witness(), - }); + let first_inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); - let second_inscription = inscription("text/png", [1; 100]); - let second_inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(2, 0, 0)], - output_count: 1, - fee: 0, - witness: second_inscription.to_witness(), - }); + let second_inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0)], + witness: inscription("text/png", [1; 100]).to_witness(), + ..Default::default() + }); - context.mine_blocks(1); + context.mine_blocks(1); - let merged_txid = context.rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(3, 1, 0), (3, 2, 0)], - output_count: 1, - ..Default::default() - }); + let merged_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(3, 1, 0), (3, 2, 0)], + ..Default::default() + }); - context.mine_blocks(1); + context.mine_blocks(1); - context.index.assert_inscription_location( - first_inscription_id, - SatPoint { - outpoint: OutPoint { - txid: merged_txid, - vout: 0, + context.index.assert_inscription_location( + first_inscription_id, + SatPoint { + outpoint: OutPoint { + txid: merged_txid, + vout: 0, + }, + offset: 0, }, - offset: 0, - }, - ); - - context.index.assert_inscription_location( - second_inscription_id, - SatPoint { - outpoint: OutPoint { - txid: merged_txid, - vout: 0, + 50 * COIN_VALUE, + ); + + context.index.assert_inscription_location( + second_inscription_id, + SatPoint { + outpoint: OutPoint { + txid: merged_txid, + vout: 0, + }, + offset: 50 * COIN_VALUE, }, - offset: 50 * COIN_VALUE, - }, - ); + 100 * COIN_VALUE, + ); + } } #[test] fn inscriptions_that_are_sent_to_second_output_are_are_tracked_correctly() { - let context = Context::new(); - context.mine_blocks(1); + for context in Context::configurations() { + context.mine_blocks(1); - let inscription = inscription("text/plain", "hello"); - let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 1, - fee: 0, - witness: inscription.to_witness(), - }); + let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); - context.mine_blocks(1); + context.mine_blocks(1); - context.index.assert_inscription_location( - inscription_id, - SatPoint { - outpoint: OutPoint { - txid: inscription_id, - vout: 0, + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { + txid: inscription_id, + vout: 0, + }, + offset: 0, }, - offset: 0, - }, - ); + 50 * COIN_VALUE, + ); - let send_txid = context.rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(2, 0, 0), (2, 1, 0)], - output_count: 2, - ..Default::default() - }); + let send_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0), (2, 1, 0)], + outputs: 2, + ..Default::default() + }); - context.mine_blocks(1); + context.mine_blocks(1); - context.index.assert_inscription_location( - inscription_id, - SatPoint { - outpoint: OutPoint { - txid: send_txid, - vout: 1, + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { + txid: send_txid, + vout: 1, + }, + offset: 0, }, - offset: 0, - }, - ); + 50 * COIN_VALUE, + ); + } } #[test] fn missing_inputs_are_fetched_from_bitcoin_core() { - let context = Context::with_args("--first-inscription-height 2"); + for args in [ + "--first-inscription-height 2", + "--first-inscription-height 2 --index-sats", + ] { + let context = Context::with_args(args); + context.mine_blocks(1); + + let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); + + context.mine_blocks(1); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { + txid: inscription_id, + vout: 0, + }, + offset: 0, + }, + 50 * COIN_VALUE, + ); + + let send_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0), (2, 1, 0)], + ..Default::default() + }); + + context.mine_blocks(1); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { + txid: send_txid, + vout: 0, + }, + offset: 50 * COIN_VALUE, + }, + 50 * COIN_VALUE, + ); + } + } + + #[test] + fn fee_spent_inscriptions_are_tracked_correctly() { + for context in Context::configurations() { + context.mine_blocks(1); + + let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); + + context.mine_blocks(1); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0)], + fee: 50 * COIN_VALUE, + ..Default::default() + }); + + let coinbase_tx = context.mine_blocks(1)[0].txdata[0].txid(); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { + txid: coinbase_tx, + vout: 0, + }, + offset: 50 * COIN_VALUE, + }, + 50 * COIN_VALUE, + ); + } + } + + #[test] + fn inscription_can_be_fee_spent_in_first_transaction() { + for context in Context::configurations() { + context.mine_blocks(1); + + let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + fee: 50 * COIN_VALUE, + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); + + let coinbase_tx = context.mine_blocks(1)[0].txdata[0].txid(); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { + txid: coinbase_tx, + vout: 0, + }, + offset: 50 * COIN_VALUE, + }, + 50 * COIN_VALUE, + ); + } + } + + #[test] + fn lost_inscriptions() { + for context in Context::configurations() { + context.mine_blocks(1); + + let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + fee: 50 * COIN_VALUE, + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); + + context.mine_blocks_with_subsidy(1, 0); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint::null(), + offset: 0, + }, + 50 * COIN_VALUE, + ); + } + } + + #[test] + fn multiple_inscriptions_can_be_lost() { + for context in Context::configurations() { + context.mine_blocks(1); + + let first_inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + fee: 50 * COIN_VALUE, + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); + + context.mine_blocks_with_subsidy(1, 0); + context.mine_blocks(1); + + let second_inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0)], + fee: 50 * COIN_VALUE, + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); + + context.mine_blocks_with_subsidy(1, 0); + + context.index.assert_inscription_location( + first_inscription_id, + SatPoint { + outpoint: OutPoint::null(), + offset: 0, + }, + 50 * COIN_VALUE, + ); + + context.index.assert_inscription_location( + second_inscription_id, + SatPoint { + outpoint: OutPoint::null(), + offset: 50 * COIN_VALUE, + }, + 150 * COIN_VALUE, + ); + } + } + + #[test] + fn lost_sats_are_tracked_correctly() { + let context = Context::with_args("--index-sats"); + assert_eq!(context.index.statistic(Statistic::LostSats), 0); + context.mine_blocks(1); + assert_eq!(context.index.statistic(Statistic::LostSats), 0); - let inscription = inscription("text/plain", "hello"); - let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 1, - fee: 0, - witness: inscription.to_witness(), - }); + context.mine_blocks_with_subsidy(1, 0); + assert_eq!( + context.index.statistic(Statistic::LostSats), + 50 * COIN_VALUE + ); + + context.mine_blocks_with_subsidy(1, 0); + assert_eq!( + context.index.statistic(Statistic::LostSats), + 100 * COIN_VALUE + ); context.mine_blocks(1); + assert_eq!( + context.index.statistic(Statistic::LostSats), + 100 * COIN_VALUE + ); + } - context.index.assert_inscription_location( - inscription_id, - SatPoint { - outpoint: OutPoint { - txid: inscription_id, - vout: 0, + #[test] + fn lost_inscriptions_get_lost_satpoints() { + for context in Context::configurations() { + context.mine_blocks_with_subsidy(1, 0); + context.mine_blocks(1); + + let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0)], + outputs: 2, + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); + context.mine_blocks(1); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(3, 1, 1), (3, 1, 0)], + fee: 50 * COIN_VALUE, + ..Default::default() + }); + context.mine_blocks_with_subsidy(1, 0); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint::null(), + offset: 75 * COIN_VALUE, }, + 100 * COIN_VALUE, + ); + } + } + + #[test] + fn inscription_skips_zero_value_first_output_of_inscribe_transaction() { + for context in Context::configurations() { + context.mine_blocks(1); + + let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + outputs: 2, + witness: inscription("text/plain", "hello").to_witness(), + output_values: &[0, 50 * COIN_VALUE], + ..Default::default() + }); + context.mine_blocks(1); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { + txid: inscription_id, + vout: 1, + }, + offset: 0, + }, + 50 * COIN_VALUE, + ); + } + } + + #[test] + fn inscription_can_be_lost_in_first_transaction() { + for context in Context::configurations() { + context.mine_blocks(1); + + let inscription_id = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + fee: 50 * COIN_VALUE, + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); + context.mine_blocks_with_subsidy(1, 0); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint::null(), + offset: 0, + }, + 50 * COIN_VALUE, + ); + } + } + + #[test] + fn lost_rare_sats_are_tracked() { + let context = Context::with_args("--index-sats"); + context.mine_blocks_with_subsidy(1, 0); + context.mine_blocks_with_subsidy(1, 0); + + assert_eq!( + context + .index + .rare_sat_satpoint(Sat(50 * COIN_VALUE)) + .unwrap() + .unwrap(), + SatPoint { + outpoint: OutPoint::null(), offset: 0, }, ); - let send_txid = context.rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(2, 0, 0), (2, 1, 0)], - output_count: 1, - ..Default::default() - }); - - context.mine_blocks(1); - - context.index.assert_inscription_location( - inscription_id, + assert_eq!( + context + .index + .rare_sat_satpoint(Sat(100 * COIN_VALUE)) + .unwrap() + .unwrap(), SatPoint { - outpoint: OutPoint { - txid: send_txid, - vout: 0, - }, + outpoint: OutPoint::null(), offset: 50 * COIN_VALUE, }, ); diff --git a/src/index/updater.rs b/src/index/updater.rs index f5e0d8a001..eb55cca25b 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -58,7 +58,7 @@ impl Updater { let mut updater = Self { cache: HashMap::new(), height, - index_sats: index.has_satoshi_index()?, + index_sats: index.has_sat_index()?, sat_ranges_since_flush: 0, outputs_cached: 0, outputs_inserted_since_flush: 0, @@ -275,28 +275,28 @@ impl Updater { let mut inscription_number_to_inscription_id = wtx.open_table(INSCRIPTION_NUMBER_TO_INSCRIPTION_ID)?; let mut outpoint_to_value = wtx.open_table(OUTPOINT_TO_VALUE)?; + 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 statistic_to_count = wtx.open_table(STATISTIC_TO_COUNT)?; - let mut next_inscription_number = inscription_number_to_inscription_id - .iter()? - .rev() - .map(|(number, _id)| number.value() + 1) - .next() + let mut lost_sats = statistic_to_count + .get(&Statistic::LostSats.key())? + .map(|lost_sats| lost_sats.value()) .unwrap_or(0); - let mut inscription_updater = InscriptionUpdater { - height: self.height, - id_to_height: &mut inscription_id_to_height, - id_to_satpoint: &mut inscription_id_to_satpoint, + let mut inscription_updater = InscriptionUpdater::new( + self.height, + &mut inscription_id_to_height, + &mut inscription_id_to_satpoint, index, - next_number: &mut next_inscription_number, - number_to_id: &mut inscription_number_to_inscription_id, - outpoint_to_value: &mut outpoint_to_value, - satpoint_to_id: &mut satpoint_to_inscription_id, - }; + lost_sats, + &mut inscription_number_to_inscription_id, + &mut outpoint_to_value, + &mut sat_to_inscription_id, + &mut satpoint_to_inscription_id, + )?; if self.index_sats { - let mut sat_to_inscription_id = wtx.open_table(SAT_TO_INSCRIPTION_ID)?; let mut sat_to_satpoint = wtx.open_table(SAT_TO_SATPOINT)?; let mut outpoint_to_sat_ranges = wtx.open_table(OUTPOINT_TO_SAT_RANGES)?; @@ -338,7 +338,6 @@ impl Updater { tx, *txid, &mut sat_to_satpoint, - &mut sat_to_inscription_id, &mut input_sat_ranges, &mut sat_ranges_written, &mut outputs_in_block, @@ -353,19 +352,36 @@ impl Updater { tx, *txid, &mut sat_to_satpoint, - &mut sat_to_inscription_id, &mut coinbase_inputs, &mut sat_ranges_written, &mut outputs_in_block, &mut inscription_updater, )?; } + + if !coinbase_inputs.is_empty() { + for (start, end) in coinbase_inputs { + if !Sat(start).is_common() { + sat_to_satpoint.insert( + &start, + &encode_satpoint(SatPoint { + outpoint: OutPoint::null(), + offset: lost_sats, + }), + )?; + } + + lost_sats += end - start; + } + } } else { - for (tx, txid) in &block.txdata { - inscription_updater.index_transaction_inscriptions(tx, *txid)?; + for (tx, txid) in block.txdata.iter().skip(1).chain(block.txdata.first()) { + lost_sats += inscription_updater.index_transaction_inscriptions(tx, *txid, None)?; } } + statistic_to_count.insert(&Statistic::LostSats.key(), &lost_sats)?; + height_to_block_hash.insert( &self.height, &block.header.block_hash().as_hash().into_inner(), @@ -387,17 +403,12 @@ impl Updater { tx: &Transaction, txid: Txid, sat_to_satpoint: &mut Table, - sat_to_inscription_id: &mut Table, input_sat_ranges: &mut VecDeque<(u64, u64)>, sat_ranges_written: &mut u64, outputs_traversed: &mut u64, inscription_updater: &mut InscriptionUpdater, ) -> Result { - if inscription_updater.index_transaction_inscriptions(tx, txid)? { - if let Some((start, _end)) = input_sat_ranges.get(0) { - sat_to_inscription_id.insert(&start, txid.as_inner())?; - } - } + inscription_updater.index_transaction_inscriptions(tx, txid, Some(input_sat_ranges))?; for (vout, output) in tx.output.iter().enumerate() { let outpoint = OutPoint { diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index eb816309bd..5f53fe2cd2 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -1,68 +1,110 @@ use super::*; +pub(super) struct Flotsam { + inscription_id: InscriptionId, + offset: u64, + old_satpoint: Option, +} + pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { - pub(super) height: u64, - pub(super) id_to_height: &'a mut Table<'db, 'tx, &'tx InscriptionIdArray, u64>, - pub(super) id_to_satpoint: &'a mut Table<'db, 'tx, &'tx InscriptionIdArray, &'tx SatPointArray>, - pub(super) index: &'a Index, - pub(super) next_number: &'a mut u64, - pub(super) number_to_id: &'a mut Table<'db, 'tx, u64, &'tx InscriptionIdArray>, - pub(super) outpoint_to_value: &'a mut Table<'db, 'tx, &'tx OutPointArray, u64>, - pub(super) satpoint_to_id: &'a mut Table<'db, 'tx, &'tx SatPointArray, &'tx InscriptionIdArray>, + flotsam: Vec, + height: u64, + id_to_height: &'a mut Table<'db, 'tx, &'tx InscriptionIdArray, u64>, + id_to_satpoint: &'a mut Table<'db, 'tx, &'tx InscriptionIdArray, &'tx SatPointArray>, + index: &'a Index, + lost_sats: u64, + next_number: u64, + number_to_id: &'a mut Table<'db, 'tx, u64, &'tx InscriptionIdArray>, + outpoint_to_value: &'a mut Table<'db, 'tx, &'tx OutPointArray, u64>, + reward: u64, + sat_to_inscription_id: &'a mut Table<'db, 'tx, u64, &'tx InscriptionIdArray>, + satpoint_to_id: &'a mut Table<'db, 'tx, &'tx SatPointArray, &'tx InscriptionIdArray>, } impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { + pub(super) fn new( + height: u64, + id_to_height: &'a mut Table<'db, 'tx, &'tx InscriptionIdArray, u64>, + id_to_satpoint: &'a mut Table<'db, 'tx, &'tx InscriptionIdArray, &'tx SatPointArray>, + index: &'a Index, + lost_sats: u64, + number_to_id: &'a mut Table<'db, 'tx, u64, &'tx InscriptionIdArray>, + outpoint_to_value: &'a mut Table<'db, 'tx, &'tx OutPointArray, u64>, + sat_to_inscription_id: &'a mut Table<'db, 'tx, u64, &'tx InscriptionIdArray>, + satpoint_to_id: &'a mut Table<'db, 'tx, &'tx SatPointArray, &'tx InscriptionIdArray>, + ) -> Result { + let next_number = number_to_id + .iter()? + .rev() + .map(|(number, _id)| number.value() + 1) + .next() + .unwrap_or(0); + + Ok(Self { + flotsam: Vec::new(), + height, + id_to_height, + id_to_satpoint, + index, + lost_sats, + next_number, + number_to_id, + outpoint_to_value, + reward: Height(height).subsidy(), + sat_to_inscription_id, + satpoint_to_id, + }) + } + pub(super) fn index_transaction_inscriptions( &mut self, tx: &Transaction, txid: Txid, - ) -> Result { - let inscribed = Inscription::from_transaction(tx).is_some(); + input_sat_ranges: Option<&VecDeque<(u64, u64)>>, + ) -> Result { + let mut inscriptions = Vec::new(); - if inscribed { - let satpoint = encode_satpoint(SatPoint { - outpoint: OutPoint { txid, vout: 0 }, + if Inscription::from_transaction(tx).is_some() { + inscriptions.push(Flotsam { + inscription_id: txid, offset: 0, + old_satpoint: None, }); - - let inscription_id = txid.as_inner(); - - self.id_to_height.insert(inscription_id, &self.height)?; - self.id_to_satpoint.insert(inscription_id, &satpoint)?; - self.satpoint_to_id.insert(&satpoint, inscription_id)?; - self.number_to_id.insert(self.next_number, inscription_id)?; - *self.next_number += 1; }; - let mut inscriptions: Vec<(u64, InscriptionIdArray, SatPointArray)> = Vec::new(); - - let mut offset = 0; + let mut input_value = 0; for tx_in in &tx.input { - let outpoint = tx_in.previous_output; - let start = encode_satpoint(SatPoint { - outpoint, - offset: 0, - }); + if tx_in.previous_output.is_null() { + input_value += Height(self.height).subsidy(); + } else { + let outpoint = tx_in.previous_output; + let start = encode_satpoint(SatPoint { + outpoint, + offset: 0, + }); - let end = encode_satpoint(SatPoint { - outpoint, - offset: u64::MAX, - }); + let end = encode_satpoint(SatPoint { + outpoint, + offset: u64::MAX, + }); - for (old_satpoint, inscription_id) in self - .satpoint_to_id - .range(start..=end)? - .map(|(satpoint, id)| (*satpoint.value(), *id.value())) - { - inscriptions.push(( - offset + decode_satpoint(old_satpoint).offset, - inscription_id, - old_satpoint, - )); - } + for (old_satpoint, inscription_id) in self + .satpoint_to_id + .range(start..=end)? + .map(|(satpoint, id)| (*satpoint.value(), *id.value())) + { + let old_satpoint = decode_satpoint(old_satpoint); + inscriptions.push(Flotsam { + offset: input_value + old_satpoint.offset, + inscription_id: InscriptionId::from_inner(inscription_id), + old_satpoint: Some(old_satpoint), + }); + } + self + .outpoint_to_value + .remove(&encode_outpoint(tx_in.previous_output))?; - if !tx_in.previous_output.is_null() { - offset += if let Some(value) = self + input_value += if let Some(value) = self .outpoint_to_value .get(&encode_outpoint(tx_in.previous_output))? { @@ -81,40 +123,44 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { .value } } + } - self - .outpoint_to_value - .remove(&encode_outpoint(tx_in.previous_output))?; + let is_coinbase = tx + .input + .first() + .map(|tx_in| tx_in.previous_output.is_null()) + .unwrap_or_default(); + + if is_coinbase { + inscriptions.append(&mut self.flotsam); } - inscriptions.sort(); + inscriptions.sort_by_key(|flotsam| flotsam.offset); let mut inscriptions = inscriptions.into_iter().peekable(); - let mut start = 0; + let mut output_value = 0; for (vout, tx_out) in tx.output.iter().enumerate() { - let end = start + tx_out.value; + let end = output_value + tx_out.value; - while let Some((offset, inscription_id, old_satpoint)) = inscriptions.peek() { - if *offset >= end { + while let Some(flotsam) = inscriptions.peek() { + if flotsam.offset >= end { break; } - let new_satpoint = encode_satpoint(SatPoint { + let new_satpoint = SatPoint { outpoint: OutPoint { txid, vout: vout.try_into().unwrap(), }, - offset: offset - start, - }); + offset: flotsam.offset - output_value, + }; - self.satpoint_to_id.remove(&old_satpoint)?; - self.satpoint_to_id.insert(&new_satpoint, &inscription_id)?; - self.id_to_satpoint.insert(&inscription_id, &new_satpoint)?; + self.update_inscription_location(input_sat_ranges, flotsam, new_satpoint)?; inscriptions.next(); } - start = end; + output_value = end; } for (vout, tx_out) in tx.output.iter().enumerate() { @@ -127,6 +173,67 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { )?; } - Ok(inscribed) + if is_coinbase { + for flotsam in inscriptions { + let new_satpoint = SatPoint { + outpoint: OutPoint::null(), + offset: self.lost_sats + flotsam.offset - output_value, + }; + self.update_inscription_location(input_sat_ranges, &flotsam, new_satpoint)?; + } + + Ok(self.reward - output_value) + } else { + self.flotsam.extend(inscriptions.map(|flotsam| Flotsam { + offset: self.reward + flotsam.offset, + ..flotsam + })); + self.reward += input_value - output_value; + Ok(0) + } + } + + fn update_inscription_location( + &mut self, + input_sat_ranges: Option<&VecDeque<(u64, u64)>>, + flotsam: &Flotsam, + new_satpoint: SatPoint, + ) -> Result { + let inscription_id = flotsam.inscription_id.into_inner(); + + match flotsam.old_satpoint { + Some(old_satpoint) => { + self.satpoint_to_id.remove(&encode_satpoint(old_satpoint))?; + } + None => { + self.id_to_height.insert(&inscription_id, &self.height)?; + self + .number_to_id + .insert(&self.next_number, &inscription_id)?; + + 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 > flotsam.offset { + self + .sat_to_inscription_id + .insert(&(start + flotsam.offset - offset), &inscription_id)?; + break; + } + offset += size; + } + } + + self.next_number += 1; + } + } + + let new_satpoint = encode_satpoint(new_satpoint); + + self.satpoint_to_id.insert(&new_satpoint, &inscription_id)?; + self.id_to_satpoint.insert(&inscription_id, &new_satpoint)?; + + Ok(()) } } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 6eb7dd72a5..b1d2d8d1d0 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -338,10 +338,7 @@ impl Server { )) })?, } - .page( - chain, - index.has_satoshi_index().map_err(ServerError::Internal)?, - ), + .page(chain, index.has_sat_index().map_err(ServerError::Internal)?), ) } @@ -366,7 +363,7 @@ impl Server { Ok( OutputHtml { outpoint, - list: if index.has_satoshi_index().map_err(ServerError::Internal)? { + list: if index.has_sat_index().map_err(ServerError::Internal)? { Some( index .list(outpoint) @@ -379,10 +376,7 @@ impl Server { chain, output, } - .page( - chain, - index.has_satoshi_index().map_err(ServerError::Internal)?, - ), + .page(chain, index.has_sat_index().map_err(ServerError::Internal)?), ) } @@ -399,10 +393,9 @@ impl Server { Ordering::Greater => Err(ServerError::BadRequest( "range start greater than range end".to_string(), )), - Ordering::Less => Ok(RangeHtml { start, end }.page( - chain, - index.has_satoshi_index().map_err(ServerError::Internal)?, - )), + Ordering::Less => Ok( + RangeHtml { start, end }.page(chain, index.has_sat_index().map_err(ServerError::Internal)?), + ), } } @@ -432,10 +425,7 @@ impl Server { .get_latest_inscriptions(8) .map_err(|err| ServerError::Internal(anyhow!("error getting inscriptions: {err}")))?, ) - .page( - chain, - index.has_satoshi_index().map_err(ServerError::Internal)?, - ), + .page(chain, index.has_sat_index().map_err(ServerError::Internal)?), ) } @@ -485,10 +475,8 @@ impl Server { }; Ok( - BlockHtml::new(block, Height(height), Self::index_height(&index)?).page( - chain, - index.has_satoshi_index().map_err(ServerError::Internal)?, - ), + BlockHtml::new(block, Height(height), Self::index_height(&index)?) + .page(chain, index.has_sat_index().map_err(ServerError::Internal)?), ) } @@ -519,10 +507,7 @@ impl Server { inscription, chain, ) - .page( - chain, - index.has_satoshi_index().map_err(ServerError::Internal)?, - ), + .page(chain, index.has_sat_index().map_err(ServerError::Internal)?), ) } @@ -640,10 +625,7 @@ impl Server { .nth(path.2) .ok_or_else(not_found)?; - Ok(InputHtml { path, input }.page( - chain, - index.has_satoshi_index().map_err(ServerError::Internal)?, - )) + Ok(InputHtml { path, input }.page(chain, index.has_sat_index().map_err(ServerError::Internal)?)) } async fn faq() -> Redirect { @@ -726,10 +708,7 @@ impl Server { inscription, satpoint, } - .page( - chain, - index.has_satoshi_index().map_err(ServerError::Internal)?, - ), + .page(chain, index.has_sat_index().map_err(ServerError::Internal)?), ) } @@ -743,10 +722,7 @@ impl Server { .get_latest_inscriptions(100) .map_err(|err| ServerError::Internal(anyhow!("error getting inscriptions: {err}")))?, } - .page( - chain, - index.has_satoshi_index().map_err(ServerError::Internal)?, - ), + .page(chain, index.has_sat_index().map_err(ServerError::Internal)?), ) } } @@ -804,7 +780,7 @@ mod tests { thread::spawn(|| server.run(options, index, ord_server_handle).unwrap()); } - while index.statistic(crate::index::Statistic::Commits).unwrap() == 0 { + while index.statistic(crate::index::Statistic::Commits) == 0 { thread::sleep(Duration::from_millis(25)); } @@ -1383,8 +1359,7 @@ mod tests { test_server.bitcoin_rpc_server.mine_blocks(1); let transaction = TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 1, + inputs: &[(1, 0, 0)], fee: 0, ..Default::default() }; @@ -1549,13 +1524,7 @@ next.*", fn commits_are_tracked() { let server = TestServer::new(); - assert_eq!( - server - .index - .statistic(crate::index::Statistic::Commits) - .unwrap(), - 1 - ); + assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 1); let info = server.index.info().unwrap(); assert_eq!(info.transactions.len(), 1); @@ -1563,13 +1532,7 @@ next.*", server.index.update().unwrap(); - assert_eq!( - server - .index - .statistic(crate::index::Statistic::Commits) - .unwrap(), - 1 - ); + assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 1); let info = server.index.info().unwrap(); assert_eq!(info.transactions.len(), 1); @@ -1580,13 +1543,7 @@ next.*", thread::sleep(Duration::from_millis(10)); server.index.update().unwrap(); - assert_eq!( - server - .index - .statistic(crate::index::Statistic::Commits) - .unwrap(), - 2 - ); + assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 2); let info = server.index.info().unwrap(); assert_eq!(info.transactions.len(), 2); @@ -1604,8 +1561,7 @@ next.*", assert_eq!( server .index - .statistic(crate::index::Statistic::OutputsTraversed) - .unwrap(), + .statistic(crate::index::Statistic::OutputsTraversed), 1 ); @@ -1614,8 +1570,7 @@ next.*", assert_eq!( server .index - .statistic(crate::index::Statistic::OutputsTraversed) - .unwrap(), + .statistic(crate::index::Statistic::OutputsTraversed), 1 ); @@ -1627,8 +1582,7 @@ next.*", assert_eq!( server .index - .statistic(crate::index::Statistic::OutputsTraversed) - .unwrap(), + .statistic(crate::index::Statistic::OutputsTraversed), 3 ); } @@ -1638,10 +1592,7 @@ next.*", let server = TestServer::new_with_args(&["--index-sats"]); assert_eq!( - server - .index - .statistic(crate::index::Statistic::SatRanges) - .unwrap(), + server.index.statistic(crate::index::Statistic::SatRanges), 1 ); @@ -1649,10 +1600,7 @@ next.*", server.index.update().unwrap(); assert_eq!( - server - .index - .statistic(crate::index::Statistic::SatRanges) - .unwrap(), + server.index.statistic(crate::index::Statistic::SatRanges), 2 ); @@ -1660,10 +1608,7 @@ next.*", server.index.update().unwrap(); assert_eq!( - server - .index - .statistic(crate::index::Statistic::SatRanges) - .unwrap(), + server.index.statistic(crate::index::Statistic::SatRanges), 3 ); } @@ -1673,17 +1618,14 @@ next.*", let server = TestServer::new_with_args(&["--index-sats"]); assert_eq!( - server - .index - .statistic(crate::index::Statistic::SatRanges) - .unwrap(), + server.index.statistic(crate::index::Statistic::SatRanges), 1 ); server.bitcoin_rpc_server.mine_blocks(1); server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 2, + inputs: &[(1, 0, 0)], + outputs: 2, fee: 0, ..Default::default() }); @@ -1691,10 +1633,7 @@ next.*", server.index.update().unwrap(); assert_eq!( - server - .index - .statistic(crate::index::Statistic::SatRanges) - .unwrap(), + server.index.statistic(crate::index::Statistic::SatRanges), 4, ); } @@ -1704,17 +1643,14 @@ next.*", let server = TestServer::new_with_args(&["--index-sats"]); assert_eq!( - server - .index - .statistic(crate::index::Statistic::SatRanges) - .unwrap(), + server.index.statistic(crate::index::Statistic::SatRanges), 1 ); server.bitcoin_rpc_server.mine_blocks(1); server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - input_slots: &[(1, 0, 0)], - output_count: 2, + inputs: &[(1, 0, 0)], + outputs: 2, fee: 2, ..Default::default() }); @@ -1722,10 +1658,7 @@ next.*", server.index.update().unwrap(); assert_eq!( - server - .index - .statistic(crate::index::Statistic::SatRanges) - .unwrap(), + server.index.statistic(crate::index::Statistic::SatRanges), 5, ); } diff --git a/test-bitcoincore-rpc/src/lib.rs b/test-bitcoincore-rpc/src/lib.rs index 209082406c..2bbfdf39c1 100644 --- a/test-bitcoincore-rpc/src/lib.rs +++ b/test-bitcoincore-rpc/src/lib.rs @@ -108,14 +108,26 @@ pub fn spawn() -> Handle { builder().build() } -#[derive(Default)] pub struct TransactionTemplate<'a> { - pub input_slots: &'a [(usize, usize, usize)], - pub output_count: usize, pub fee: u64, + pub inputs: &'a [(usize, usize, usize)], + pub output_values: &'a [u64], + pub outputs: usize, pub witness: Witness, } +impl<'a> Default for TransactionTemplate<'a> { + fn default() -> Self { + Self { + fee: 0, + inputs: &[], + output_values: &[], + outputs: 1, + witness: Witness::default(), + } + } +} + pub struct Handle { close_handle: Option, port: u16, @@ -135,22 +147,19 @@ impl Handle { self.state().wallets.clone() } - pub fn mine_blocks(&self, num: u64) -> Vec { - let mut bitcoin_rpc_data = self.state.lock().unwrap(); - (0..num) - .map(|_| bitcoin_rpc_data.push_block(50 * COIN_VALUE)) - .collect() + pub fn mine_blocks(&self, n: u64) -> Vec { + self.mine_blocks_with_subsidy(n, 50 * COIN_VALUE) } - pub fn mine_blocks_with_subsidy(&self, num: u64, subsidy: u64) -> Vec { + pub fn mine_blocks_with_subsidy(&self, n: u64, subsidy: u64) -> Vec { let mut bitcoin_rpc_data = self.state.lock().unwrap(); - (0..num) + (0..n) .map(|_| bitcoin_rpc_data.push_block(subsidy)) .collect() } - pub fn broadcast_tx(&self, options: TransactionTemplate) -> Txid { - self.state().broadcast_tx(options) + pub fn broadcast_tx(&self, template: TransactionTemplate) -> Txid { + self.state().broadcast_tx(template) } pub fn invalidate_tip(&self) -> BlockHash { diff --git a/test-bitcoincore-rpc/src/state.rs b/test-bitcoincore-rpc/src/state.rs index 707513c78e..a74f570a50 100644 --- a/test-bitcoincore-rpc/src/state.rs +++ b/test-bitcoincore-rpc/src/state.rs @@ -122,10 +122,10 @@ impl State { blockhash } - pub(crate) fn broadcast_tx(&mut self, options: TransactionTemplate) -> Txid { + 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 options.input_slots.iter().enumerate() { + for (i, (height, tx, vout)) in template.inputs.iter().enumerate() { let tx = &self.blocks.get(&self.hashes[*height]).unwrap().txdata[*tx]; total_value += tx.output[*vout].value; input.push(TxIn { @@ -133,16 +133,16 @@ impl State { script_sig: Script::new(), sequence: Sequence::MAX, witness: if i == 0 { - options.witness.clone() + template.witness.clone() } else { Witness::new() }, }); } - let value_per_output = (total_value - options.fee) / options.output_count as u64; + let value_per_output = (total_value - template.fee) / template.outputs as u64; assert_eq!( - value_per_output * options.output_count as u64 + options.fee, + value_per_output * template.outputs as u64 + template.fee, total_value ); @@ -150,9 +150,13 @@ impl State { version: 0, lock_time: PackedLockTime(0), input, - output: (0..options.output_count) - .map(|_| TxOut { - value: value_per_output, + output: (0..template.outputs) + .map(|i| TxOut { + value: template + .output_values + .get(i) + .cloned() + .unwrap_or(value_per_output), script_pubkey: script::Builder::new().into_script(), }) .collect(),