From 950740aebc28891424e590fd6eb2a50536cea6f1 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 24 May 2023 03:19:04 -0700 Subject: [PATCH] Unbind inscriptions from zero-sat transactions (#2107) --- src/index.rs | 160 ++++++++++++++++------- src/index/updater.rs | 15 ++- src/index/updater/inscription_updater.rs | 31 ++++- src/lib.rs | 7 + src/subcommand/server.rs | 57 +++++++- src/templates/inscription.rs | 51 ++++++-- templates/inscription.html | 6 +- 7 files changed, 253 insertions(+), 74 deletions(-) diff --git a/src/index.rs b/src/index.rs index 6cd46192b2..6ea644a88d 100644 --- a/src/index.rs +++ b/src/index.rs @@ -69,6 +69,7 @@ pub(crate) enum Statistic { LostSats = 2, OutputsTraversed = 3, SatRanges = 4, + UnboundInscriptions = 5, } impl Statistic { @@ -801,7 +802,7 @@ impl Index { &self, inscription_id: InscriptionId, satpoint: SatPoint, - sat: u64, + sat: Option, ) { let rtx = self.database.begin_read().unwrap(); @@ -836,32 +837,34 @@ impl Index { inscription_id, ); - if self.has_sat_index().unwrap() { - assert_eq!( - InscriptionId::load( - *rtx - .open_table(SAT_TO_INSCRIPTION_ID) - .unwrap() - .get(&sat) - .unwrap() - .unwrap() - .value() - ), - inscription_id, - ); - - assert_eq!( - SatPoint::load( - *rtx - .open_table(SAT_TO_SATPOINT) - .unwrap() - .get(&sat) - .unwrap() - .unwrap() - .value() - ), - satpoint, - ); + if let Some(sat) = sat { + if self.has_sat_index().unwrap() { + assert_eq!( + InscriptionId::load( + *rtx + .open_table(SAT_TO_INSCRIPTION_ID) + .unwrap() + .get(&sat) + .unwrap() + .unwrap() + .value() + ), + inscription_id, + ); + + assert_eq!( + SatPoint::load( + *rtx + .open_table(SAT_TO_SATPOINT) + .unwrap() + .get(&sat) + .unwrap() + .unwrap() + .value() + ), + satpoint, + ); + } } } @@ -1378,7 +1381,70 @@ mod tests { outpoint: OutPoint { txid, vout: 0 }, offset: 0, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), + ); + } + } + + #[test] + fn inscriptions_without_sats_are_unbound() { + for context in Context::configurations() { + context.mine_blocks(1); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + fee: 50 * 100_000_000, + ..Default::default() + }); + + context.mine_blocks(1); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0)], + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); + + let inscription_id = InscriptionId::from(txid); + + context.mine_blocks(1); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: unbound_outpoint(), + offset: 0, + }, + None, + ); + + context.mine_blocks(1); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(4, 0, 0)], + fee: 50 * 100_000_000, + ..Default::default() + }); + + context.mine_blocks(1); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(5, 1, 0)], + witness: inscription("text/plain", "hello").to_witness(), + ..Default::default() + }); + + let inscription_id = InscriptionId::from(txid); + + context.mine_blocks(1); + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: unbound_outpoint(), + offset: 1, + }, + None, ); } } @@ -1403,7 +1469,7 @@ mod tests { outpoint: OutPoint { txid, vout: 0 }, offset: 0, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); let send_txid = context.rpc_server.broadcast_tx(TransactionTemplate { @@ -1422,7 +1488,7 @@ mod tests { }, offset: 50 * COIN_VALUE, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); } } @@ -1465,7 +1531,7 @@ mod tests { }, offset: 0, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); context.index.assert_inscription_location( @@ -1477,7 +1543,7 @@ mod tests { }, offset: 50 * COIN_VALUE, }, - 100 * COIN_VALUE, + Some(100 * COIN_VALUE), ); } } @@ -1502,7 +1568,7 @@ mod tests { outpoint: OutPoint { txid, vout: 0 }, offset: 0, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); let send_txid = context.rpc_server.broadcast_tx(TransactionTemplate { @@ -1522,7 +1588,7 @@ mod tests { }, offset: 0, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); } } @@ -1551,7 +1617,7 @@ mod tests { outpoint: OutPoint { txid, vout: 0 }, offset: 0, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); let send_txid = context.rpc_server.broadcast_tx(TransactionTemplate { @@ -1570,7 +1636,7 @@ mod tests { }, offset: 50 * COIN_VALUE, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); } } @@ -1606,7 +1672,7 @@ mod tests { }, offset: 50 * COIN_VALUE, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); } } @@ -1642,7 +1708,7 @@ mod tests { }, offset: 50 * COIN_VALUE, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); } } @@ -1671,7 +1737,7 @@ mod tests { }, offset: 50 * COIN_VALUE, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); } } @@ -1697,7 +1763,7 @@ mod tests { outpoint: OutPoint::null(), offset: 0, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); } } @@ -1734,7 +1800,7 @@ mod tests { outpoint: OutPoint::null(), offset: 0, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); context.index.assert_inscription_location( @@ -1743,7 +1809,7 @@ mod tests { outpoint: OutPoint::null(), offset: 50 * COIN_VALUE, }, - 150 * COIN_VALUE, + Some(150 * COIN_VALUE), ); } } @@ -1854,7 +1920,7 @@ mod tests { outpoint: OutPoint::null(), offset: 75 * COIN_VALUE, }, - 100 * COIN_VALUE, + Some(100 * COIN_VALUE), ); } } @@ -1880,7 +1946,7 @@ mod tests { outpoint: OutPoint { txid, vout: 1 }, offset: 0, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); } } @@ -1905,7 +1971,7 @@ mod tests { outpoint: OutPoint::null(), offset: 0, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); } } @@ -2089,7 +2155,7 @@ mod tests { }, offset: 0, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); let second = context.rpc_server.broadcast_tx(TransactionTemplate { @@ -2109,7 +2175,7 @@ mod tests { }, offset: 0, }, - 50 * COIN_VALUE, + Some(50 * COIN_VALUE), ); assert!(context diff --git a/src/index/updater.rs b/src/index/updater.rs index adfa210ec9..76e6a6ecc9 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -417,6 +417,11 @@ impl Updater { .map(|lost_sats| lost_sats.value()) .unwrap_or(0); + let unbound_inscriptions = statistic_to_count + .get(&Statistic::UnboundInscriptions.key())? + .map(|unbound_inscriptions| unbound_inscriptions.value()) + .unwrap_or(0); + let mut inscription_updater = InscriptionUpdater::new( self.height, &mut inscription_id_to_satpoint, @@ -428,6 +433,7 @@ impl Updater { &mut sat_to_inscription_id, &mut satpoint_to_inscription_id, block.header.time, + unbound_inscriptions, value_cache, )?; @@ -523,11 +529,16 @@ impl Updater { } } else { for (tx, txid) in block.txdata.iter().skip(1).chain(block.txdata.first()) { - lost_sats += inscription_updater.index_transaction_inscriptions(tx, *txid, None)?; + inscription_updater.index_transaction_inscriptions(tx, *txid, None)?; } } - statistic_to_count.insert(&Statistic::LostSats.key(), &lost_sats)?; + statistic_to_count.insert(&Statistic::LostSats.key(), &inscription_updater.lost_sats)?; + + statistic_to_count.insert( + &Statistic::UnboundInscriptions.key(), + &inscription_updater.unbound_inscriptions, + )?; height_to_block_hash.insert(&self.height, &block.header.block_hash().store())?; diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index cdb375471b..46fccfa4c6 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -17,7 +17,7 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { id_to_satpoint: &'a mut Table<'db, 'tx, &'static InscriptionIdValue, &'static SatPointValue>, value_receiver: &'a mut Receiver, id_to_entry: &'a mut Table<'db, 'tx, &'static InscriptionIdValue, InscriptionEntryValue>, - lost_sats: u64, + pub(super) lost_sats: u64, next_number: u64, number_to_id: &'a mut Table<'db, 'tx, u64, &'static InscriptionIdValue>, outpoint_to_value: &'a mut Table<'db, 'tx, &'static OutPointValue, u64>, @@ -25,6 +25,7 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { sat_to_inscription_id: &'a mut Table<'db, 'tx, u64, &'static InscriptionIdValue>, satpoint_to_id: &'a mut Table<'db, 'tx, &'static SatPointValue, &'static InscriptionIdValue>, timestamp: u32, + pub(super) unbound_inscriptions: u64, value_cache: &'a mut HashMap, } @@ -40,6 +41,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { sat_to_inscription_id: &'a mut Table<'db, 'tx, u64, &'static InscriptionIdValue>, satpoint_to_id: &'a mut Table<'db, 'tx, &'static SatPointValue, &'static InscriptionIdValue>, timestamp: u32, + unbound_inscriptions: u64, value_cache: &'a mut HashMap, ) -> Result { let next_number = number_to_id @@ -63,6 +65,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { sat_to_inscription_id, satpoint_to_id, timestamp, + unbound_inscriptions, value_cache, }) } @@ -72,7 +75,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { tx: &Transaction, txid: Txid, input_sat_ranges: Option<&VecDeque<(u64, u64)>>, - ) -> Result { + ) -> Result { let mut inscriptions = Vec::new(); let mut input_value = 0; @@ -111,13 +114,27 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { if inscriptions.iter().all(|flotsam| flotsam.offset != 0) && Inscription::from_transaction(tx).is_some() { - inscriptions.push(Flotsam { + let flotsam = Flotsam { inscription_id: txid.into(), offset: 0, origin: Origin::New { fee: input_value - tx.output.iter().map(|txout| txout.value).sum::(), }, - }); + }; + + if input_value == 0 { + self.update_inscription_location( + input_sat_ranges, + flotsam, + SatPoint { + outpoint: unbound_outpoint(), + offset: self.unbound_inscriptions, + }, + )?; + self.unbound_inscriptions += 1; + } else { + inscriptions.push(flotsam); + } }; let is_coinbase = tx @@ -176,15 +193,15 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { }; self.update_inscription_location(input_sat_ranges, flotsam, new_satpoint)?; } - - Ok(self.reward - output_value) + self.lost_sats += self.reward - output_value; + Ok(()) } else { self.flotsam.extend(inscriptions.map(|flotsam| Flotsam { offset: self.reward + flotsam.offset - output_value, ..flotsam })); self.reward += input_value - output_value; - Ok(0) + Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index 3d0e873ace..c3875abefc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,6 +141,13 @@ fn timestamp(seconds: u32) -> DateTime { Utc.timestamp_opt(seconds.into(), 0).unwrap() } +fn unbound_outpoint() -> OutPoint { + OutPoint { + txid: Hash::all_zeros(), + vout: 0, + } +} + pub fn main() { env_logger::init(); diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 98d827868f..9b38b27ad4 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -829,13 +829,19 @@ impl Server { .get_inscription_satpoint_by_id(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - let output = index - .get_transaction(satpoint.outpoint.txid)? - .ok_or_not_found(|| format!("inscription {inscription_id} current transaction"))? - .output - .into_iter() - .nth(satpoint.outpoint.vout.try_into().unwrap()) - .ok_or_not_found(|| format!("inscription {inscription_id} current transaction output"))?; + let output = if satpoint.outpoint == unbound_outpoint() { + None + } else { + Some( + index + .get_transaction(satpoint.outpoint.txid)? + .ok_or_not_found(|| format!("inscription {inscription_id} current transaction"))? + .output + .into_iter() + .nth(satpoint.outpoint.vout.try_into().unwrap()) + .ok_or_not_found(|| format!("inscription {inscription_id} current transaction output"))?, + ) + }; let previous = if let Some(previous) = entry.number.checked_sub(1) { Some( @@ -1599,6 +1605,43 @@ mod tests { ); } + #[test] + fn unbound_output_recieves_unbound_inscriptions() { + let server = TestServer::new_with_regtest(); + + server.mine_blocks(1); + + server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + fee: 50 * 100_000_000, + ..Default::default() + }); + + server.mine_blocks(1); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0)], + witness: inscription("text/plain;charset=utf-8", "hello").to_witness(), + ..Default::default() + }); + + server.mine_blocks(1); + + let inscription_id = InscriptionId::from(txid); + + server.assert_response_regex( + format!("/inscription/{}", inscription_id), + StatusCode::OK, + format!( + ".*
+
id
+
{inscription_id}
+
preview
.*
output
+
0000000000000000000000000000000000000000000000000000000000000000:0
.*" + ), + ); + } + #[test] fn unknown_output_returns_404() { TestServer::new().assert_response( diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index 0f396903fe..9c76b22c38 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -9,7 +9,7 @@ pub(crate) struct InscriptionHtml { pub(crate) inscription_id: InscriptionId, pub(crate) next: Option, pub(crate) number: u64, - pub(crate) output: TxOut, + pub(crate) output: Option, pub(crate) previous: Option, pub(crate) sat: Option, pub(crate) satpoint: SatPoint, @@ -31,7 +31,7 @@ mod tests { use super::*; #[test] - fn without_sat_or_nav_links() { + fn without_sat_nav_links_or_output() { assert_regex_match!( InscriptionHtml { chain: Chain::Mainnet, @@ -41,7 +41,7 @@ mod tests { inscription_id: inscription_id(1), next: None, number: 1, - output: tx_out(1, address()), + output: None, previous: None, sat: None, satpoint: satpoint(1, 0), @@ -57,10 +57,6 @@ mod tests {
id
1{64}i1
-
address
-
bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
-
output value
-
1
preview
link
content
@@ -89,6 +85,43 @@ mod tests { ); } + #[test] + fn with_output() { + assert_regex_match!( + InscriptionHtml { + chain: Chain::Mainnet, + genesis_fee: 1, + genesis_height: 0, + inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"), + inscription_id: inscription_id(1), + next: None, + number: 1, + output: Some(tx_out(1, address())), + previous: None, + sat: None, + satpoint: satpoint(1, 0), + timestamp: timestamp(0), + }, + " +

Inscription 1

+
+
+ +
+
+
+ .* +
address
+
bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
+
output value
+
1
+ .* +
+ " + .unindent() + ); + } + #[test] fn with_sat() { assert_regex_match!( @@ -100,7 +133,7 @@ mod tests { inscription_id: inscription_id(1), next: None, number: 1, - output: tx_out(1, address()), + output: Some(tx_out(1, address())), previous: None, sat: Some(Sat(1)), satpoint: satpoint(1, 0), @@ -132,7 +165,7 @@ mod tests { inscription_id: inscription_id(2), next: Some(inscription_id(3)), number: 1, - output: tx_out(1, address()), + output: Some(tx_out(1, address())), previous: Some(inscription_id(1)), sat: None, satpoint: satpoint(1, 0), diff --git a/templates/inscription.html b/templates/inscription.html index 37573f3706..e205f6f6bd 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -15,12 +15,14 @@

Inscription {{ self.number }}

id
{{ self.inscription_id }}
-%% if let Ok(address) = self.chain.address_from_script(&self.output.script_pubkey ) { +%% if let Some(output) = &self.output { +%% if let Ok(address) = self.chain.address_from_script(&output.script_pubkey ) {
address
{{ address }}
%% }
output value
-
{{ self.output.value }}
+
{{ output.value }}
+%% } %% if let Some(sat) = self.sat {
sat
{{sat}}