diff --git a/src/index.rs b/src/index.rs index 7f5d297317..c0c3699a5c 100644 --- a/src/index.rs +++ b/src/index.rs @@ -14,6 +14,8 @@ impl Index { TableDefinition::new("HEIGHT_TO_HASH"); const OUTPOINT_TO_ORDINAL_RANGES: TableDefinition<'static, [u8], [u8]> = TableDefinition::new("OUTPOINT_TO_ORDINAL_RANGES"); + const KEY_TO_SATPOINT: TableDefinition<'static, [u8], [u8]> = + TableDefinition::new("KEY_TO_SATPOINT"); pub(crate) fn open(options: Options) -> Result { let client = Client::new( @@ -128,6 +130,7 @@ impl Index { } let mut outpoint_to_ordinal_ranges = wtx.open_table(&Self::OUTPOINT_TO_ORDINAL_RANGES)?; + let mut key_to_satpoint = wtx.open_table(&Self::KEY_TO_SATPOINT)?; let mut coinbase_inputs = VecDeque::new(); @@ -137,7 +140,7 @@ impl Index { coinbase_inputs.push_front((start.n(), (start + h.subsidy()).n())); } - for tx in block.txdata.iter().skip(1) { + for (tx_offset, tx) in block.txdata.iter().enumerate().skip(1) { let mut input_ordinal_ranges = VecDeque::new(); for input in &tx.input { @@ -155,85 +158,94 @@ impl Index { } } - for (vout, output) in tx.output.iter().enumerate() { - let mut ordinals = Vec::new(); + self.index_transaction( + height, + tx_offset as u64, + tx, + &mut input_ordinal_ranges, + &mut outpoint_to_ordinal_ranges, + &mut key_to_satpoint, + )?; - let mut remaining = output.value; - while remaining > 0 { - let range = input_ordinal_ranges - .pop_front() - .ok_or("Found transaction with outputs but no inputs")?; - - let count = range.1 - range.0; - - let assigned = if count > remaining { - let middle = range.0 + remaining; - input_ordinal_ranges.push_front((middle, range.1)); - (range.0, middle) - } else { - range - }; - - ordinals.extend_from_slice(&assigned.0.to_le_bytes()); - ordinals.extend_from_slice(&assigned.1.to_le_bytes()); - - remaining -= assigned.1 - assigned.0; - } - - let outpoint = OutPoint { - txid: tx.txid(), - vout: vout as u32, - }; - - let mut key = Vec::new(); - outpoint.consensus_encode(&mut key)?; - - outpoint_to_ordinal_ranges.insert(&key, &ordinals)?; - } - - coinbase_inputs.extend(&input_ordinal_ranges); + coinbase_inputs.extend(input_ordinal_ranges); } if let Some(tx) = block.txdata.first() { - for (vout, output) in tx.output.iter().enumerate() { - let mut ordinals = Vec::new(); - - let mut remaining = output.value; - while remaining > 0 { - let range = coinbase_inputs - .pop_front() - .ok_or("Insufficient inputs for coinbase transaction outputs")?; - - let count = range.1 - range.0; + self.index_transaction( + height, + 0, + tx, + &mut coinbase_inputs, + &mut outpoint_to_ordinal_ranges, + &mut key_to_satpoint, + )?; + } - let assigned = if count > remaining { - let middle = range.0 + remaining; - coinbase_inputs.push_front((middle, range.1)); - (range.0, middle) - } else { - range - }; + height_to_hash.insert(&height, &block.block_hash())?; + wtx.commit()?; + } - ordinals.extend_from_slice(&assigned.0.to_le_bytes()); - ordinals.extend_from_slice(&assigned.1.to_le_bytes()); + Ok(()) + } - remaining -= assigned.1 - assigned.0; + fn index_transaction( + &self, + block: u64, + tx_offset: u64, + tx: &Transaction, + input_ordinal_ranges: &mut VecDeque<(u64, u64)>, + outpoint_to_ordinal_ranges: &mut Table<[u8], [u8]>, + key_to_satpoint: &mut Table<[u8], [u8]>, + ) -> Result { + for (vout, output) in tx.output.iter().enumerate() { + let outpoint = OutPoint { + txid: tx.txid(), + vout: vout as u32, + }; + let mut outpoint_encoded = Vec::new(); + outpoint.consensus_encode(&mut outpoint_encoded)?; + + let mut ordinals = Vec::new(); + + let mut remaining = output.value; + while remaining > 0 { + let range = input_ordinal_ranges + .pop_front() + .ok_or("Insufficient inputs for transaction outputs")?; + + let count = range.1 - range.0; + + let assigned = if count > remaining { + let middle = range.0 + remaining; + input_ordinal_ranges.push_front((middle, range.1)); + (range.0, middle) + } else { + range + }; + + let mut satpoint = Vec::new(); + SatPoint { + offset: output.value - remaining, + outpoint, + } + .consensus_encode(&mut satpoint)?; + key_to_satpoint.insert( + &Key { + ordinal: assigned.0, + block, + transaction: tx_offset, } + .encode(), + &satpoint, + )?; - let outpoint = OutPoint { - txid: tx.txid(), - vout: vout as u32, - }; - - let mut key = Vec::new(); - outpoint.consensus_encode(&mut key)?; + ordinals.extend_from_slice(&assigned.0.to_le_bytes()); + ordinals.extend_from_slice(&assigned.1.to_le_bytes()); - outpoint_to_ordinal_ranges.insert(&key, &ordinals)?; - } + remaining -= assigned.1 - assigned.0; } - height_to_hash.insert(&height, &block.block_hash())?; - wtx.commit()?; + outpoint_to_ordinal_ranges.insert(&outpoint_encoded, &ordinals)?; } Ok(()) @@ -249,6 +261,47 @@ impl Index { } } + pub(crate) fn find(&self, ordinal: Ordinal) -> Result> { + let rtx = self.database.begin_read()?; + + let height_to_hash = match rtx.open_table(&Self::HEIGHT_TO_HASH) { + Ok(height_to_hash) => height_to_hash, + Err(redb::Error::TableDoesNotExist(_)) => return Ok(None), + Err(err) => return Err(err.into()), + }; + + if let Some((height, _hash)) = height_to_hash.range_reversed(0..)?.next() { + if height < ordinal.height().0 { + return Ok(None); + } + } + + let key_to_satpoint = match rtx.open_table(&Self::KEY_TO_SATPOINT) { + Ok(key_to_satpoint) => key_to_satpoint, + Err(redb::Error::TableDoesNotExist(_)) => return Ok(None), + Err(err) => return Err(err.into()), + }; + + match key_to_satpoint + .range_reversed([].as_slice()..=Key::new(ordinal).encode().as_slice())? + .next() + { + Some((start_key, start_satpoint)) => { + let start_key = Key::decode(start_key)?; + let start_satpoint = SatPoint::consensus_decode(start_satpoint)?; + Ok(Some(( + start_key.block, + start_key.transaction, + SatPoint { + offset: start_satpoint.offset + (ordinal.0 - start_key.ordinal), + outpoint: start_satpoint.outpoint, + }, + ))) + } + None => Ok(None), + } + } + pub(crate) fn list(&self, outpoint: OutPoint) -> Result> { let rtx = self.database.begin_read()?; let outpoint_to_ordinal_ranges = rtx.open_table(&Self::OUTPOINT_TO_ORDINAL_RANGES)?; diff --git a/src/key.rs b/src/key.rs new file mode 100644 index 0000000000..9a23587afa --- /dev/null +++ b/src/key.rs @@ -0,0 +1,50 @@ +use super::*; + +pub(crate) struct Key { + pub(crate) ordinal: u64, + pub(crate) block: u64, + pub(crate) transaction: u64, +} + +impl Key { + pub(crate) fn new(ordinal: Ordinal) -> Key { + Self { + ordinal: ordinal.0, + block: u64::max_value(), + transaction: u64::max_value(), + } + } + + pub(crate) fn encode(self) -> Vec { + let mut buffer = Vec::new(); + buffer.extend(self.ordinal.to_be_bytes()); + buffer.extend(self.block.to_be_bytes()); + buffer.extend(self.transaction.to_be_bytes()); + buffer + } + + pub(crate) fn decode(buffer: &[u8]) -> Result { + if buffer.len() != 24 { + return Err("Buffer too small to decode key from".into()); + } + + Ok(Key { + ordinal: u64::from_be_bytes(buffer[0..8].try_into().unwrap()), + block: u64::from_be_bytes(buffer[8..16].try_into().unwrap()), + transaction: u64::from_be_bytes(buffer[16..24].try_into().unwrap()), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decode_error() { + assert_eq!( + Key::decode(&[]).err().unwrap().to_string(), + "Buffer too small to decode key from" + ); + } +} diff --git a/src/main.rs b/src/main.rs index bf01c2652c..3fbaebdacf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,17 @@ use { crate::{ - arguments::Arguments, bytes::Bytes, epoch::Epoch, height::Height, index::Index, + arguments::Arguments, bytes::Bytes, epoch::Epoch, height::Height, index::Index, key::Key, options::Options, ordinal::Ordinal, sat_point::SatPoint, subcommand::Subcommand, }, - bitcoin::{blockdata::constants::COIN_VALUE, consensus::Encodable, Block, OutPoint, Transaction}, + bitcoin::{ + blockdata::constants::COIN_VALUE, consensus::Decodable, consensus::Encodable, Block, OutPoint, + Transaction, + }, clap::Parser, derive_more::{Display, FromStr}, integer_cbrt::IntegerCubeRoot, integer_sqrt::IntegerSquareRoot, - redb::{Database, ReadableTable, TableDefinition}, + redb::{Database, ReadableTable, Table, TableDefinition}, std::{ cell::Cell, cmp::Ordering, @@ -28,6 +31,7 @@ mod bytes; mod epoch; mod height; mod index; +mod key; mod options; mod ordinal; mod sat_point; diff --git a/src/sat_point.rs b/src/sat_point.rs index a2530bbedb..969e792727 100644 --- a/src/sat_point.rs +++ b/src/sat_point.rs @@ -5,27 +5,24 @@ pub(crate) struct SatPoint { pub(crate) offset: u64, } -impl SatPoint { - pub(crate) fn from_transaction_and_offset(tx: &Transaction, mut offset: u64) -> SatPoint { - for (vout, output) in tx.output.iter().enumerate() { - if output.value > offset { - return SatPoint { - outpoint: OutPoint { - txid: tx.txid(), - vout: vout.try_into().unwrap(), - }, - offset, - }; - } - offset -= output.value; - } +impl Display for SatPoint { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}:{}", self.outpoint, self.offset) + } +} - panic!("Could not find ordinal in transaction!"); +impl Encodable for SatPoint { + fn consensus_encode(&self, mut s: S) -> Result { + let len = self.outpoint.consensus_encode(&mut s)?; + Ok(len + self.offset.consensus_encode(s)?) } } -impl Display for SatPoint { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}:{}", self.outpoint, self.offset) +impl Decodable for SatPoint { + fn consensus_decode(mut d: D) -> Result { + Ok(SatPoint { + outpoint: Decodable::consensus_decode(&mut d)?, + offset: Decodable::consensus_decode(d)?, + }) } } diff --git a/src/subcommand/find.rs b/src/subcommand/find.rs index f7c3cda0c8..18f85c7ea6 100644 --- a/src/subcommand/find.rs +++ b/src/subcommand/find.rs @@ -2,8 +2,6 @@ use super::*; #[derive(Parser)] pub(crate) struct Find { - #[clap(long)] - as_of_height: u64, #[clap(long)] slot: bool, ordinal: Ordinal, @@ -11,37 +9,21 @@ pub(crate) struct Find { impl Find { pub(crate) fn run(self, options: Options) -> Result<()> { - let index = Index::open(options)?; - - let creation_height = self.ordinal.height().n(); - let block = index.block(creation_height)?.unwrap(); - - let offset = self.ordinal.subsidy_position(); - let mut satpoint = SatPoint::from_transaction_and_offset(&block.txdata[0], offset); - let mut slot = (creation_height, 0, satpoint.outpoint.vout, offset); + let index = Index::index(options)?; - for height in (creation_height + 1)..(self.as_of_height + 1) { - match index.block(height)? { - Some(block) => { - for (txindex, transaction) in block.txdata.iter().enumerate() { - for input in &transaction.input { - if input.previous_output == satpoint.outpoint { - satpoint = SatPoint::from_transaction_and_offset(transaction, satpoint.offset); - slot = (height, txindex, satpoint.outpoint.vout, satpoint.offset); - } - } - } + match index.find(self.ordinal)? { + Some((block, tx, satpoint)) => { + if self.slot { + println!( + "{block}.{tx}.{}.{}", + satpoint.outpoint.vout, satpoint.offset + ); + } else { + println!("{satpoint}"); } - None => break, + Ok(()) } + None => Err("Ordinal has not been mined as of index height".into()), } - - if self.slot { - println!("{}.{}.{}.{}", slot.0, slot.1, slot.2, slot.3); - } else { - println!("{satpoint}"); - } - - Ok(()) } } diff --git a/tests/find.rs b/tests/find.rs index 5165ad0153..279c2f24c3 100644 --- a/tests/find.rs +++ b/tests/find.rs @@ -3,7 +3,7 @@ use super::*; #[test] fn first_satoshi() -> Result { Test::new()? - .command("find 0 --as-of-height 0") + .command("find 0") .expected_stdout("0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0:0\n") .block() .run() @@ -12,7 +12,7 @@ fn first_satoshi() -> Result { #[test] fn first_satoshi_slot() -> Result { Test::new()? - .command("find 0 --as-of-height 0 --slot") + .command("find 0 --slot") .expected_stdout("0.0.0.0\n") .block() .run() @@ -21,7 +21,7 @@ fn first_satoshi_slot() -> Result { #[test] fn second_satoshi() -> Result { Test::new()? - .command("find 1 --as-of-height 0") + .command("find 1") .expected_stdout("0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0:1\n") .block() .run() @@ -30,7 +30,7 @@ fn second_satoshi() -> Result { #[test] fn second_satoshi_slot() -> Result { Test::new()? - .command("find 1 --as-of-height 0 --slot") + .command("find 1 --slot") .expected_stdout("0.0.0.1\n") .block() .run() @@ -39,7 +39,7 @@ fn second_satoshi_slot() -> Result { #[test] fn first_satoshi_of_second_block() -> Result { Test::new()? - .command("find 5000000000 --as-of-height 1") + .command("find 5000000000") .expected_stdout("9068a11b8769174363376b606af9a4b8b29dd7b13d013f4b0cbbd457db3c3ce5:0:0\n") .block() .block() @@ -49,7 +49,7 @@ fn first_satoshi_of_second_block() -> Result { #[test] fn first_satoshi_of_second_block_slot() -> Result { Test::new()? - .command("find 5000000000 --as-of-height 1 --slot") + .command("find 5000000000 --slot") .expected_stdout("1.0.0.0\n") .block() .block() @@ -59,7 +59,7 @@ fn first_satoshi_of_second_block_slot() -> Result { #[test] fn first_satoshi_spent_in_second_block() -> Result { Test::new()? - .command("find 0 --as-of-height 1") + .command("find 0") .expected_stdout("d0a9c70e6c8d890ee5883973a716edc1609eab42a9bc32594bdafc935bb4fad0:0:0\n") .block() .block() @@ -74,7 +74,7 @@ fn first_satoshi_spent_in_second_block() -> Result { #[test] fn first_satoshi_spent_in_second_block_slot() -> Result { Test::new()? - .command("find 0 --as-of-height 1 --slot") + .command("find 0 --slot") .expected_stdout("1.1.0.0\n") .block() .block() @@ -89,7 +89,7 @@ fn first_satoshi_spent_in_second_block_slot() -> Result { #[test] fn regression_empty_block_crash() -> Result { Test::new()? - .command("find 0 --slot --as-of-height 1") + .command("find 0 --slot") .block() .block_with_coinbase(CoinbaseOptions { include_coinbase_transaction: false, @@ -102,7 +102,7 @@ fn regression_empty_block_crash() -> Result { #[test] fn mining_and_spending_transaction_in_same_block() -> Result { Test::new()? - .command("find 0 --as-of-height 1 --slot") + .command("find 0 --slot") .block() .block() .transaction(TransactionOptions { @@ -118,3 +118,22 @@ fn mining_and_spending_transaction_in_same_block() -> Result { .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() -> Result { + Test::new()? + .block() + .expected_stderr("error: Ordinal has not been mined as of index height\n") + .expected_status(1) + .command("find 5000000000") + .run() +} diff --git a/tests/index.rs b/tests/index.rs index d8fc7a3b70..607b13e2bb 100644 --- a/tests/index.rs +++ b/tests/index.rs @@ -3,7 +3,7 @@ use super::*; #[test] fn default_index_size() -> Result { let tempdir = Test::new()? - .command("find 0 --as-of-height 0") + .command("find 0") .expected_stdout("0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0:0\n") .block() .output()? @@ -39,7 +39,7 @@ fn incremental_indexing() -> Result { #[test] fn custom_index_size() -> Result { let tempdir = Test::new()? - .command("--index-size 2097152 find 0 --as-of-height 0") + .command("--index-size 2097152 find 0") .expected_stdout("0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0:0\n") .block() .output()? @@ -53,7 +53,7 @@ fn custom_index_size() -> Result { #[test] fn human_readable_index_size() -> Result { let tempdir = Test::new()? - .command("--index-size 2mib find 0 --as-of-height 0") + .command("--index-size 2mib find 0") .expected_stdout("0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef9:0:0\n") .block() .output()?