diff --git a/src/index.rs b/src/index.rs index 74585da93a..2585fc65b9 100644 --- a/src/index.rs +++ b/src/index.rs @@ -43,7 +43,7 @@ impl Index { self.database.print_info() } - fn decode_ordinal_range(bytes: [u8; 11]) -> (u64, u64) { + pub(crate) fn decode_ordinal_range(bytes: [u8; 11]) -> (u64, u64) { let n = u128::from_le_bytes([ bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], 0, 0, 0, 0, 0, @@ -228,6 +228,14 @@ impl Index { } } + pub(crate) fn find(&self, ordinal: Ordinal) -> Result> { + if self.database.height()? <= ordinal.height().0 { + return Ok(None); + } + + self.database.find(ordinal) + } + pub(crate) fn list(&self, outpoint: OutPoint) -> Result>> { let mut outpoint_encoded = Vec::new(); outpoint.consensus_encode(&mut outpoint_encoded)?; diff --git a/src/lmdb_database.rs b/src/lmdb_database.rs index 97f16feba7..6957e9aaa0 100644 --- a/src/lmdb_database.rs +++ b/src/lmdb_database.rs @@ -101,6 +101,31 @@ impl Database { .map(|ranges| ranges.to_vec()), ) } + + pub(crate) fn find(&self, ordinal: Ordinal) -> Result> { + let tx = lmdb::ReadTransaction::new(self.environment.clone())?; + + let access = tx.access(); + + let mut cursor = tx.cursor(&self.outpoint_to_ordinal_ranges)?; + + while let Some((key, value)) = cursor.next::<[u8], [u8]>(&access).into_option()? { + let mut offset = 0; + for chunk in value.chunks_exact(11) { + let (start, end) = Index::decode_ordinal_range(chunk.try_into().unwrap()); + if start <= ordinal.0 && ordinal.0 < end { + let outpoint: OutPoint = Decodable::consensus_decode(key)?; + return Ok(Some(SatPoint { + outpoint, + offset: offset + ordinal.0 - start, + })); + } + offset += end - start; + } + } + + Ok(None) + } } pub(crate) struct WriteTransaction<'a> { diff --git a/src/main.rs b/src/main.rs index f68af3aa6e..c4b757886b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use { crate::{ arguments::Arguments, bytes::Bytes, epoch::Epoch, height::Height, index::Index, - options::Options, ordinal::Ordinal, subcommand::Subcommand, + options::Options, ordinal::Ordinal, sat_point::SatPoint, subcommand::Subcommand, }, anyhow::{anyhow, Context, Error}, axum::{extract, http::StatusCode, response::IntoResponse, routing::get, Json, Router}, diff --git a/src/redb_database.rs b/src/redb_database.rs index eba3d9b4f2..eed365852a 100644 --- a/src/redb_database.rs +++ b/src/redb_database.rs @@ -64,6 +64,46 @@ impl Database { Ok(()) } + pub(crate) fn height(&self) -> Result { + let tx = self.0.begin_read()?; + + let height_to_hash = tx.open_table(&HEIGHT_TO_HASH)?; + + Ok( + height_to_hash + .range(0..)? + .rev() + .next() + .map(|(height, _hash)| height + 1) + .unwrap_or(0), + ) + } + + pub(crate) fn find(&self, ordinal: Ordinal) -> Result> { + let rtx = self.0.begin_read()?; + + let outpoint_to_ordinal_ranges = rtx.open_table(&OUTPOINT_TO_ORDINAL_RANGES)?; + + let mut cursor = outpoint_to_ordinal_ranges.range([]..)?; + + while let Some((key, value)) = cursor.next() { + let mut offset = 0; + for chunk in value.chunks_exact(11) { + let (start, end) = Index::decode_ordinal_range(chunk.try_into().unwrap()); + if start <= ordinal.0 && ordinal.0 < end { + let outpoint: OutPoint = Decodable::consensus_decode(key)?; + return Ok(Some(SatPoint { + outpoint, + offset: offset + ordinal.0 - start, + })); + } + offset += end - start; + } + } + + Ok(None) + } + pub(crate) fn list(&self, outpoint: &[u8]) -> Result>> { Ok( self diff --git a/src/subcommand.rs b/src/subcommand.rs index 780aa6da2d..2e56300cc0 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -1,6 +1,7 @@ use super::*; mod epochs; +mod find; mod index; mod info; mod list; @@ -13,6 +14,7 @@ mod traits; #[derive(Parser)] pub(crate) enum Subcommand { Epochs, + Find(find::Find), Index, List(list::List), Name(name::Name), @@ -27,6 +29,7 @@ impl Subcommand { pub(crate) fn run(self, options: Options) -> Result<()> { match self { Self::Epochs => epochs::run(), + Self::Find(find) => find.run(options), Self::Index => index::run(options), Self::List(list) => list.run(options), Self::Name(name) => name.run(), diff --git a/src/subcommand/find.rs b/src/subcommand/find.rs new file mode 100644 index 0000000000..9275f3912d --- /dev/null +++ b/src/subcommand/find.rs @@ -0,0 +1,20 @@ +use super::*; + +#[derive(Parser)] +pub(crate) struct Find { + ordinal: Ordinal, +} + +impl Find { + pub(crate) fn run(self, options: Options) -> Result<()> { + let index = Index::index(&options)?; + + match index.find(self.ordinal)? { + Some(satpoint) => { + println!("{satpoint}"); + Ok(()) + } + None => Err(anyhow!("Ordinal has not been mined as of index height")), + } + } +} diff --git a/tests/find.rs b/tests/find.rs new file mode 100644 index 0000000000..b9ce8f4d48 --- /dev/null +++ b/tests/find.rs @@ -0,0 +1,154 @@ +use super::*; + +#[test] +fn first_satoshi() -> Result { + Test::new()? + .command("find 0") + .expected_stdout("0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0:0\n") + .block() + .run() +} + +#[test] +#[ignore] +fn first_satoshi_slot() -> Result { + Test::new()? + .command("find 0 --slot") + .expected_stdout("0.0.0.0\n") + .block() + .run() +} + +#[test] +fn second_satoshi() -> Result { + Test::new()? + .command("find 1") + .expected_stdout("0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0:1\n") + .block() + .run() +} + +#[test] +#[ignore] +fn second_satoshi_slot() -> Result { + Test::new()? + .command("find 1 --slot") + .expected_stdout("0.0.0.1\n") + .block() + .run() +} + +#[test] +fn first_satoshi_of_second_block() -> Result { + Test::new()? + .command("find 5000000000") + .expected_stdout("9068a11b8769174363376b606af9a4b8b29dd7b13d013f4b0cbbd457db3c3ce5:0:0\n") + .block() + .block() + .run() +} + +#[test] +#[ignore] +fn first_satoshi_of_second_block_slot() -> Result { + Test::new()? + .command("find 5000000000 --slot") + .expected_stdout("1.0.0.0\n") + .block() + .block() + .run() +} + +#[test] +fn first_satoshi_spent_in_second_block() -> Result { + Test::new()? + .command("find 0") + .expected_stdout("d0a9c70e6c8d890ee5883973a716edc1609eab42a9bc32594bdafc935bb4fad0:0:0\n") + .block() + .block() + .transaction(TransactionOptions { + slots: &[(0, 0, 0)], + output_count: 1, + fee: 0, + }) + .run() +} + +#[test] +#[ignore] +fn first_satoshi_spent_in_second_block_slot() -> Result { + Test::new()? + .command("find 0 --slot") + .expected_stdout("1.1.0.0\n") + .block() + .block() + .transaction(TransactionOptions { + slots: &[(0, 0, 0)], + output_count: 1, + fee: 0, + }) + .run() +} + +#[test] +#[ignore] +fn regression_empty_block_crash() -> Result { + Test::new()? + .command("find 0 --slot") + .block() + .block_with_coinbase(CoinbaseOptions { + include_coinbase_transaction: false, + ..Default::default() + }) + .expected_stdout("0.0.0.0\n") + .run() +} + +#[test] +#[ignore] +fn mining_and_spending_transaction_in_same_block() -> Result { + Test::new()? + .command("find 0 --slot") + .block() + .block() + .transaction(TransactionOptions { + slots: &[(0, 0, 0)], + output_count: 1, + fee: 0, + }) + .transaction(TransactionOptions { + slots: &[(1, 1, 0)], + output_count: 1, + fee: 0, + }) + .expected_stdout("1.2.0.0\n") + .run() +} + +#[test] +fn empty_index() -> Result { + Test::new()? + .expected_stderr("error: Ordinal has not been mined as of index height\n") + .expected_status(1) + .command("find 0") + .run() +} + +#[test] +fn unmined_satoshi_in_second_block() -> Result { + Test::new()? + .block() + .expected_stderr("error: Ordinal has not been mined as of index height\n") + .expected_status(1) + .command("find 5000000000") + .run() +} + +#[test] +fn unmined_satoshi_in_first_block() -> Result { + Test::new()? + .expected_stderr("error: Ordinal has not been mined as of index height\n") + .expected_status(1) + .command("find 0") + .run() +} diff --git a/tests/index.rs b/tests/index.rs index 2ef6b8a539..0a1d2adcdc 100644 --- a/tests/index.rs +++ b/tests/index.rs @@ -26,8 +26,8 @@ fn incremental_indexing() -> Result { #[cfg(feature = "redb")] fn custom_index_size() -> Result { let tempdir = Test::new()? - .command("--index-size 2097152 list 0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0") - .expected_stdout("[0,5000000000)\n") + .command("--index-size 2097152 find 0") + .expected_stdout("0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0:0\n") .block() .output()? .tempdir; @@ -41,10 +41,8 @@ fn custom_index_size() -> Result { #[cfg(feature = "redb")] fn human_readable_index_size() -> Result { let tempdir = Test::new()? - .command( - "--index-size 2mib list 0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0", - ) - .expected_stdout("[0,5000000000)\n") + .command("--index-size 2mib find 0") + .expected_stdout("0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0:0\n") .block() .output()? .tempdir; @@ -58,8 +56,8 @@ fn human_readable_index_size() -> Result { #[cfg(feature = "redb")] fn default_index_size() -> Result { let tempdir = Test::new()? - .command("list 0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0") - .expected_stdout("[0,5000000000)\n") + .command("find 0") + .expected_stdout("0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0:0\n") .block() .output()? .tempdir; diff --git a/tests/integration.rs b/tests/integration.rs index e8fd7fded5..d18db24ed4 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -27,6 +27,7 @@ use { }; mod epochs; +mod find; mod index; mod info; mod list;