diff --git a/Cargo.lock b/Cargo.lock index 3dacd3de07..8a5b13343f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,9 +153,9 @@ checksum = "3ebc5a6d89e3c90b84e8f33c8737933dda8f1c106b5415900b38b9d433841478" [[package]] name = "fastrand" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] @@ -230,9 +230,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.112" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" [[package]] name = "log" @@ -320,9 +320,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" dependencies = [ "proc-macro2", ] @@ -423,9 +423,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" dependencies = [ "clap", "lazy_static", @@ -447,9 +447,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" dependencies = [ "proc-macro2", "quote", diff --git a/src/command.rs b/src/command.rs index 311b0f65fd..903e18939b 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2,6 +2,8 @@ use super::*; mod epochs; mod find; +mod list; +mod name; mod range; mod supply; mod traits; @@ -9,39 +11,24 @@ mod traits; #[derive(StructOpt)] pub(crate) enum Command { Epochs, - Find { - #[structopt(long)] - blocksdir: Option, - ordinal: Ordinal, - height: u64, - }, - Name { - name: String, - }, - Range { - #[structopt(long)] - name: bool, - height: Height, - }, + Find(find::Find), + Name(name::Name), + List(list::List), + Range(range::Range), Supply, - Traits { - ordinal: Ordinal, - }, + Traits(traits::Traits), } impl Command { pub(crate) fn run(self) -> Result<()> { match self { Self::Epochs => epochs::run(), - Self::Find { - blocksdir, - ordinal, - height, - } => find::run(blocksdir.as_deref(), ordinal, height), - Self::Name { name } => name::run(&name), - Self::Range { height, name } => range::run(height, name), + Self::Find(find) => find.run(), + Self::Name(name) => name.run(), + Self::List(list) => list.run(), + Self::Range(range) => range.run(), Self::Supply => supply::run(), - Self::Traits { ordinal } => traits::run(ordinal), + Self::Traits(traits) => traits.run(), } } } diff --git a/src/command/find.rs b/src/command/find.rs index a9a42cd14a..6ba1502a36 100644 --- a/src/command/find.rs +++ b/src/command/find.rs @@ -1,22 +1,49 @@ use super::*; -pub(crate) fn run(blocksdir: Option<&Path>, ordinal: Ordinal, at_height: u64) -> Result<()> { - let index = Index::new(blocksdir)?; +#[derive(StructOpt)] +pub(crate) struct Find { + #[structopt(long)] + blocksdir: Option, + #[structopt(long)] + as_of_height: u64, + #[structopt(long)] + slot: bool, + ordinal: Ordinal, +} + +impl Find { + pub(crate) fn run(self) -> Result<()> { + let index = Index::new(self.blocksdir.as_deref())?; - let height = ordinal.height().n(); - assert!(height < 100); - assert!(height == at_height); + let creation_height = self.ordinal.height().n(); + let block = index.block(creation_height)?.unwrap(); - let block = index.block(height)?; + 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 mut offset = ordinal.subsidy_position(); - for (index, output) in block.txdata[0].output.iter().enumerate() { - if output.value > offset { - println!("{}:{index}:{offset}", block.txdata[0].txid()); - break; + 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); + } + } + } + } + None => break, + } } - offset -= output.value; - } - Ok(()) + if self.slot { + println!("{}.{}.{}.{}", slot.0, slot.1, slot.2, slot.3); + } else { + println!("{satpoint}"); + } + + Ok(()) + } } diff --git a/src/command/list.rs b/src/command/list.rs new file mode 100644 index 0000000000..7519ecb354 --- /dev/null +++ b/src/command/list.rs @@ -0,0 +1,21 @@ +use super::*; + +#[derive(StructOpt)] +pub(crate) struct List { + #[structopt(long)] + blocksdir: Option, + outpoint: OutPoint, +} + +impl List { + pub(crate) fn run(self) -> Result<()> { + let index = Index::new(self.blocksdir.as_deref())?; + let ranges = index.list(self.outpoint)?; + + for (start, end) in ranges { + println!("[{start},{end})"); + } + + Ok(()) + } +} diff --git a/src/command/name.rs b/src/command/name.rs new file mode 100644 index 0000000000..87080afda9 --- /dev/null +++ b/src/command/name.rs @@ -0,0 +1,47 @@ +use super::*; + +#[derive(StructOpt)] +pub(crate) struct Name { + name: String, +} + +impl Name { + pub(crate) fn run(self) -> Result { + for c in self.name.chars() { + if !('a'..='z').contains(&c) { + return Err("Invalid name".into()); + } + } + + let mut min = 0; + let mut max = 2099999997690000; + let mut guess = max / 2; + + loop { + log::info!("min max guess: {} {} {}", min, max, guess); + + let name = Ordinal::new(guess).name(); + + match name + .len() + .cmp(&self.name.len()) + .then(name.deref().cmp(&self.name)) + .reverse() + { + Ordering::Less => min = guess + 1, + Ordering::Equal => break, + Ordering::Greater => max = guess, + } + + if max - min == 0 { + return Err("Name out of range".into()); + } + + guess = min + (max - min) / 2; + } + + println!("{}", guess); + + Ok(()) + } +} diff --git a/src/command/range.rs b/src/command/range.rs index af35849c06..75dba0e586 100644 --- a/src/command/range.rs +++ b/src/command/range.rs @@ -1,31 +1,40 @@ use super::*; -pub(crate) fn run(height: Height, name_range: bool) -> Result { - let mut start = 0; +#[derive(StructOpt)] +pub(crate) struct Range { + #[structopt(long)] + name: bool, + height: Height, +} + +impl Range { + pub(crate) fn run(self) -> Result { + let mut start = 0; - for n in 0..height.n() { - let subsidy = Height(n).subsidy(); + for n in 0..self.height.n() { + let subsidy = Height(n).subsidy(); - if subsidy == 0 { - break; + if subsidy == 0 { + break; + } + + start += subsidy; } - start += subsidy; - } + let end = start + self.height.subsidy(); - let end = start + height.subsidy(); - - if name_range { - let (start, end) = match (Ordinal::new_checked(start), Ordinal::new_checked(end)) { - (Some(start), Some(end)) => (start.name(), end.name()), - (Some(start), None) => (start.name(), start.name()), - (None, None) => (Ordinal::LAST.name(), Ordinal::LAST.name()), - (None, Some(_)) => unreachable!(), - }; - println!("[{},{})", start, end); - } else { - println!("[{},{})", start, end); - } + if self.name { + let (start, end) = match (Ordinal::new_checked(start), Ordinal::new_checked(end)) { + (Some(start), Some(end)) => (start.name(), end.name()), + (Some(start), None) => (start.name(), start.name()), + (None, None) => (Ordinal::LAST.name(), Ordinal::LAST.name()), + (None, Some(_)) => unreachable!(), + }; + println!("[{},{})", start, end); + } else { + println!("[{},{})", start, end); + } - Ok(()) + Ok(()) + } } diff --git a/src/command/traits.rs b/src/command/traits.rs index 4a8468f20c..7bd09fdae9 100644 --- a/src/command/traits.rs +++ b/src/command/traits.rs @@ -1,70 +1,77 @@ use super::*; -pub(crate) fn run(ordinal: Ordinal) -> Result { - let n = ordinal.n(); +#[derive(StructOpt)] +pub(crate) struct Traits { + ordinal: Ordinal, +} - if n % 2 == 0 { - println!("even"); - } else { - println!("odd"); - } +impl Traits { + pub(crate) fn run(self) -> Result { + let n = self.ordinal.n(); - let isqrt = n.integer_sqrt(); - if isqrt * isqrt == n { - println!("square"); - } + if n % 2 == 0 { + println!("even"); + } else { + println!("odd"); + } - let icbrt = n.integer_cbrt(); - if icbrt * icbrt * icbrt == n { - println!("cube"); - } + let isqrt = n.integer_sqrt(); + if isqrt * isqrt == n { + println!("square"); + } - let digits = n.to_string().chars().collect::>(); + let icbrt = n.integer_cbrt(); + if icbrt * icbrt * icbrt == n { + println!("cube"); + } - let pi = std::f64::consts::PI.to_string().replace('.', ""); - let s = n.to_string(); - if s == pi[..s.len()] { - println!("pi"); - } + let digits = n.to_string().chars().collect::>(); - if digits.chunks(2).all(|chunk| chunk == ['6', '9']) { - println!("nice"); - } + let pi = std::f64::consts::PI.to_string().replace('.', ""); + let s = n.to_string(); + if s == pi[..s.len()] { + println!("pi"); + } - if digits.iter().all(|c| *c == '7') { - println!("angelic"); - } + if digits.chunks(2).all(|chunk| chunk == ['6', '9']) { + println!("nice"); + } - println!( - "luck: {}/{}", - digits.iter().filter(|c| **c == '8').count() as i64 - - digits.iter().filter(|c| **c == '4').count() as i64, - digits.len() - ); + if digits.iter().all(|c| *c == '7') { + println!("angelic"); + } - println!("population: {}", ordinal.population()); + println!( + "luck: {}/{}", + digits.iter().filter(|c| **c == '8').count() as i64 + - digits.iter().filter(|c| **c == '4').count() as i64, + digits.len() + ); - println!("name: {}", ordinal.name()); + println!("population: {}", self.ordinal.population()); - if let Some(character) = char::from_u32((n % 0x110000) as u32) { - println!("character: {:?}", character); - } + println!("name: {}", self.ordinal.name()); - println!("epoch: {}", ordinal.epoch()); + if let Some(character) = char::from_u32((n % 0x110000) as u32) { + println!("character: {:?}", character); + } - println!("height: {}", ordinal.height()); + println!("epoch: {}", self.ordinal.epoch()); - if ordinal.subsidy_position() == 0 { - println!("shiny"); - } + println!("height: {}", self.ordinal.height()); - if ordinal.height() == 124724 { - if ordinal == 623624999999999 { - println!("illusive"); - } else { - println!("cursed"); + if self.ordinal.subsidy_position() == 0 { + println!("shiny"); } - } - Ok(()) + if self.ordinal.height() == 124724 { + if self.ordinal == 623624999999999 { + println!("illusive"); + } else { + println!("cursed"); + } + } + + Ok(()) + } } diff --git a/src/height.rs b/src/height.rs index 8ad841f804..c57065f2b8 100644 --- a/src/height.rs +++ b/src/height.rs @@ -11,6 +11,13 @@ impl Height { pub(crate) fn subsidy(self) -> u64 { Epoch::from(self).subsidy() } + + pub(crate) fn starting_ordinal(self) -> Option { + let epoch = Epoch::from(self); + let epoch_starting_ordinal = epoch.starting_ordinal()?; + let epoch_starting_height = epoch.starting_height(); + Some(epoch_starting_ordinal + (self - epoch_starting_height.n()).n() * epoch.subsidy()) + } } impl Add for Height { @@ -85,4 +92,23 @@ mod tests { assert_eq!(Height(210000).subsidy(), 2500000000); assert_eq!(Height(210000 + 1).subsidy(), 2500000000); } + + #[test] + fn starting_ordinal() { + assert_eq!(Height(0).starting_ordinal().unwrap(), 0); + assert_eq!(Height(1).starting_ordinal().unwrap(), 5000000000); + assert_eq!( + Height(210000 - 1).starting_ordinal().unwrap(), + (210000 - 1) * 5000000000 + ); + assert_eq!( + Height(210000).starting_ordinal().unwrap(), + 210000 * 5000000000 + ); + assert_eq!( + Height(210000 + 1).starting_ordinal().unwrap(), + 210000 * 5000000000 + 2500000000 + ); + assert_eq!(Height(u64::max_value()).starting_ordinal(), None); + } } diff --git a/src/index.rs b/src/index.rs index 8c725fc1d2..2248024f9b 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1,16 +1,17 @@ use super::*; -const CHILDREN: &str = "children"; -const HEIGHTS: &str = "heights"; -const BLOCK_OFFSETS: &str = "block_offsets"; -const HEIGHTS_TO_HASHES: &str = "height_to_hashes"; - pub(crate) struct Index { blocksdir: PathBuf, database: Database, } impl Index { + const HASH_TO_CHILDREN: &'static str = "HASH_TO_CHILDREN"; + const HASH_TO_HEIGHT: &'static str = "HASH_TO_HEIGHT"; + const HASH_TO_OFFSET: &'static str = "HASH_TO_OFFSET"; + const HEIGHT_TO_HASH: &'static str = "HEIGHT_TO_HASH"; + const OUTPOINT_TO_ORDINAL_RANGES: &'static str = "OUTPOINT_TO_ORDINAL_RANGES"; + pub(crate) fn new(blocksdir: Option<&Path>) -> Result { let blocksdir = if let Some(blocksdir) = blocksdir { blocksdir.to_owned() @@ -35,16 +36,116 @@ impl Index { index.index_blockfile()?; + index.index_ranges()?; + Ok(index) } + fn index_ranges(&self) -> Result { + let mut height = 0; + while let Some(block) = self.block(height)? { + let wtx = self.database.begin_write()?; + let mut outpoint_to_ordinal_ranges: Table<[u8], [u8]> = + wtx.open_table(Self::OUTPOINT_TO_ORDINAL_RANGES)?; + + let mut coinbase_inputs = VecDeque::new(); + + let h = Height(height); + if let Some(start) = h.starting_ordinal() { + coinbase_inputs.push_front((start.n(), (start + h.subsidy()).n())); + } + + for tx in &block.txdata[1..] { + let mut input_ordinal_ranges = VecDeque::new(); + for input in &tx.input { + let mut key = Vec::new(); + input.previous_output.consensus_encode(&mut key)?; + let ordinal_ranges = outpoint_to_ordinal_ranges + .get(key.as_slice())? + .ok_or("Could not find outpoint in index")?; + + for chunk in ordinal_ranges.to_value().chunks_exact(16) { + let start = u64::from_le_bytes(chunk[0..8].try_into().unwrap()); + let end = u64::from_le_bytes(chunk[8..16].try_into().unwrap()); + input_ordinal_ranges.push_back((start, end)); + } + } + for (vout, output) in tx.output.iter().enumerate() { + let mut ordinals = Vec::new(); + 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 split = range.0 + remaining; + input_ordinal_ranges.push_front((split, range.1)); + (range.0, split) + } 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); + } + + { + let tx = &block.txdata[0]; + 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; + let assigned = if count > remaining { + let split = range.0 + remaining; + coinbase_inputs.push_front((split, range.1)); + (range.0, split) + } 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)?; + } + } + + wtx.commit()?; + height += 1; + } + + Ok(()) + } + fn index_blockfile(&self) -> Result { { let tx = self.database.begin_write()?; - let mut children: MultimapTable<[u8], [u8]> = tx.open_multimap_table(CHILDREN)?; + let mut hash_to_children: MultimapTable<[u8], [u8]> = + tx.open_multimap_table(Self::HASH_TO_CHILDREN)?; - let mut block_offsets: Table<[u8], u64> = tx.open_table(BLOCK_OFFSETS)?; + let mut hash_to_offset: Table<[u8], u64> = tx.open_table(Self::HASH_TO_OFFSET)?; let blocks = fs::read(self.blocksdir.join("blk00000.dat"))?; @@ -61,9 +162,9 @@ impl Index { let block = Block::consensus_decode(&blocks[range.clone()])?; - children.insert(&block.header.prev_blockhash, &block.block_hash())?; + hash_to_children.insert(&block.header.prev_blockhash, &block.block_hash())?; - block_offsets.insert(&block.block_hash(), &(offset as u64))?; + hash_to_offset.insert(&block.block_hash(), &(offset as u64))?; offset = range.end; @@ -78,15 +179,16 @@ impl Index { { let write = self.database.begin_write()?; - let mut heights: Table<[u8], u64> = write.open_table(HEIGHTS)?; - let mut heights_to_hashes: Table = write.open_table(HEIGHTS_TO_HASHES)?; + let mut hash_to_height: Table<[u8], u64> = write.open_table(Self::HASH_TO_HEIGHT)?; + let mut height_to_hash: Table = write.open_table(Self::HEIGHT_TO_HASH)?; - heights.insert(genesis_block(Network::Bitcoin).block_hash().deref(), &0)?; - heights_to_hashes.insert(&0, genesis_block(Network::Bitcoin).block_hash().deref())?; + hash_to_height.insert(genesis_block(Network::Bitcoin).block_hash().deref(), &0)?; + height_to_hash.insert(&0, genesis_block(Network::Bitcoin).block_hash().deref())?; let read = self.database.begin_read()?; - let children: ReadOnlyMultimapTable<[u8], [u8]> = read.open_multimap_table(CHILDREN)?; + let hash_to_children: ReadOnlyMultimapTable<[u8], [u8]> = + read.open_multimap_table(Self::HASH_TO_CHILDREN)?; let mut queue = vec![( genesis_block(Network::Bitcoin) @@ -97,10 +199,10 @@ impl Index { )]; while let Some((block, height)) = queue.pop() { - heights.insert(block.as_ref(), &height)?; - heights_to_hashes.insert(&height, block.as_ref())?; + hash_to_height.insert(block.as_ref(), &height)?; + height_to_hash.insert(&height, block.as_ref())?; - let mut iter = children.get(&block)?; + let mut iter = hash_to_children.get(&block)?; while let Some(child) = iter.next() { queue.push((child.to_vec(), height + 1)); @@ -113,19 +215,27 @@ impl Index { Ok(()) } - pub(crate) fn block(&self, height: u64) -> Result { + pub(crate) fn block(&self, height: u64) -> Result> { let tx = self.database.begin_read()?; - let heights_to_hashes: ReadOnlyTable = tx.open_table(HEIGHTS_TO_HASHES)?; - let guard = heights_to_hashes.get(&height)?.unwrap(); - let hash = guard.to_value(); + let heights_to_hash: ReadOnlyTable = tx.open_table(Self::HEIGHT_TO_HASH)?; - let offsets: ReadOnlyTable<[u8], u64> = tx.open_table(BLOCK_OFFSETS)?; - let offset = offsets.get(hash)?.unwrap().to_value() as usize; + match heights_to_hash.get(&height)? { + None => Ok(None), + Some(guard) => { + let hash = guard.to_value(); - let blocks = fs::read(self.blocksdir.join("blk00000.dat"))?; + let hash_to_offset: ReadOnlyTable<[u8], u64> = tx.open_table(Self::HASH_TO_OFFSET)?; + let offset = hash_to_offset + .get(hash)? + .ok_or("Could not find offset to block in index")? + .to_value() as usize; - Self::decode_block_at(&blocks, offset) + let blocks = fs::read(self.blocksdir.join("blk00000.dat"))?; + + Ok(Some(Self::decode_block_at(&blocks, offset)?)) + } + } } fn block_range_at(blocks: &[u8], offset: usize) -> Result> { @@ -143,4 +253,22 @@ impl Index { &blocks[Self::block_range_at(blocks, offset)?], )?) } + + pub(crate) fn list(&self, outpoint: OutPoint) -> Result> { + let rtx = self.database.begin_read()?; + let outpoint_to_ordinal_ranges: ReadOnlyTable<[u8], [u8]> = + rtx.open_table(Self::OUTPOINT_TO_ORDINAL_RANGES)?; + let mut key = Vec::new(); + outpoint.consensus_encode(&mut key)?; + let ordinal_ranges = outpoint_to_ordinal_ranges + .get(key.as_slice())? + .ok_or("Could not find outpoint in index")?; + let mut output = Vec::new(); + for chunk in ordinal_ranges.to_value().chunks_exact(16) { + let start = u64::from_le_bytes(chunk[0..8].try_into().unwrap()); + let end = u64::from_le_bytes(chunk[8..16].try_into().unwrap()); + output.push((start, end)); + } + Ok(output) + } } diff --git a/src/main.rs b/src/main.rs index 4517f488f3..06173b1c6a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ use { - crate::{epoch::Epoch, height::Height, index::Index, ordinal::Ordinal}, + crate::{epoch::Epoch, height::Height, index::Index, ordinal::Ordinal, sat_point::SatPoint}, bitcoin::{ blockdata::constants::{genesis_block, COIN_VALUE}, - consensus::Decodable, - Block, Network, + consensus::{Decodable, Encodable}, + Block, Network, OutPoint, Transaction, }, derive_more::Display, integer_cbrt::IntegerCubeRoot, @@ -14,6 +14,8 @@ use { }, std::{ cmp::Ordering, + collections::VecDeque, + fmt::{self, Display, Formatter}, fs, ops::{Add, AddAssign, Deref, Range, Sub}, path::{Path, PathBuf}, @@ -27,8 +29,8 @@ mod command; mod epoch; mod height; mod index; -mod name; mod ordinal; +mod sat_point; type Result> = std::result::Result; diff --git a/src/name.rs b/src/name.rs deleted file mode 100644 index 686f754199..0000000000 --- a/src/name.rs +++ /dev/null @@ -1,40 +0,0 @@ -use super::*; - -pub(crate) fn run(needle: &str) -> Result { - for c in needle.chars() { - if !('a'..='z').contains(&c) { - return Err("Invalid name".into()); - } - } - - let mut min = 0; - let mut max = 2099999997690000; - let mut guess = max / 2; - - loop { - log::info!("min max guess: {} {} {}", min, max, guess); - - let name = Ordinal::new(guess).name(); - - match name - .len() - .cmp(&needle.len()) - .then(name.deref().cmp(needle)) - .reverse() - { - Ordering::Less => min = guess + 1, - Ordering::Equal => break, - Ordering::Greater => max = guess, - } - - if max - min == 0 { - return Err("Name out of range".into()); - } - - guess = min + (max - min) / 2; - } - - println!("{}", guess); - - Ok(()) -} diff --git a/src/sat_point.rs b/src/sat_point.rs new file mode 100644 index 0000000000..a2530bbedb --- /dev/null +++ b/src/sat_point.rs @@ -0,0 +1,31 @@ +use super::*; + +pub(crate) struct SatPoint { + pub(crate) outpoint: OutPoint, + 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; + } + + panic!("Could not find ordinal in transaction!"); + } +} + +impl Display for SatPoint { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}:{}", self.outpoint, self.offset) + } +} diff --git a/tests/find.rs b/tests/find.rs index 70a829d425..75f63cb555 100644 --- a/tests/find.rs +++ b/tests/find.rs @@ -3,23 +3,63 @@ use super::*; #[test] fn first_satoshi() -> Result { Test::new()? - .args(&["find", "--blocksdir", "blocks", "0", "0"]) + .command("find --blocksdir blocks 0 --as-of-height 0") .expected_stdout("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0\n") .run() } +#[test] +fn first_satoshi_slot() -> Result { + Test::new()? + .command("find --blocksdir blocks 0 --as-of-height 0 --slot") + .expected_stdout("0.0.0.0\n") + .run() +} + #[test] fn second_satoshi() -> Result { Test::new()? - .args(&["find", "--blocksdir", "blocks", "1", "0"]) + .command("find --blocksdir blocks 1 --as-of-height 0") .expected_stdout("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:1\n") .run() } +#[test] +fn second_satoshi_slot() -> Result { + Test::new()? + .command("find --blocksdir blocks 1 --as-of-height 0 --slot") + .expected_stdout("0.0.0.1\n") + .run() +} + #[test] fn first_satoshi_of_second_block() -> Result { Test::new()? - .args(&["find", "--blocksdir", "blocks", "5000000000", "1"]) + .command("find --blocksdir blocks 5000000000 --as-of-height 1") .expected_stdout("e5fb252959bdc7727c80296dbc53e1583121503bb2e266a609ebc49cf2a74c1d:0:0\n") .run() } + +#[test] +fn first_satoshi_of_second_block_slot() -> Result { + Test::new()? + .command("find --blocksdir blocks 5000000000 --as-of-height 1 --slot") + .expected_stdout("1.0.0.0\n") + .run() +} + +#[test] +fn first_satoshi_spent_in_second_block() -> Result { + Test::new()? + .command("find --blocksdir blocks 0 --as-of-height 1") + .expected_stdout("1e8149c3be0dd66b1cbcb4652d15bea04a9bc8d515c4f544e71bb35a9cba1ed0:0:0\n") + .run() +} + +#[test] +fn first_satoshi_spent_in_second_block_slot() -> Result { + Test::new()? + .command("find --blocksdir blocks 0 --as-of-height 1 --slot") + .expected_stdout("1.1.0.0\n") + .run() +} diff --git a/tests/integration.rs b/tests/integration.rs index 6d620e4610..60e5ec703e 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -21,6 +21,7 @@ use { mod epochs; mod find; +mod list; mod name; mod range; mod supply; @@ -49,6 +50,13 @@ impl Test { }) } + fn command(self, args: &str) -> Self { + Self { + args: args.split_whitespace().map(str::to_owned).collect(), + ..self + } + } + fn args(self, args: &[&str]) -> Self { Self { args: self @@ -120,7 +128,7 @@ impl Test { } } -fn generate_transaction(height: usize) -> Transaction { +fn generate_coinbase_transaction(height: usize) -> Transaction { // Base let mut ret = Transaction { version: 1, @@ -151,6 +159,35 @@ fn generate_transaction(height: usize) -> Transaction { ret } +fn generate_spending_transaction(previous_output: OutPoint) -> Transaction { + // Base + let mut ret = Transaction { + version: 1, + lock_time: 0, + input: vec![], + output: vec![], + }; + + // Inputs + let in_script = script::Builder::new().into_script(); + ret.input.push(TxIn { + script_sig: in_script, + sequence: MAX_SEQUENCE, + witness: vec![], + previous_output, + }); + + // Outputs + let out_script = script::Builder::new().into_script(); + ret.output.push(TxOut { + value: 50 * COIN_VALUE, + script_pubkey: out_script, + }); + + // end + ret +} + fn serialize_block(output: &mut File, block: &Block) -> io::Result<()> { output.write_all(&[0xf9, 0xbe, 0xb4, 0xd9])?; let size_field = output.stream_position()?; @@ -159,7 +196,6 @@ fn serialize_block(output: &mut File, block: &Block) -> io::Result<()> { output.seek(SeekFrom::Start(size_field))?; output.write_all(&(size as u32).to_le_bytes())?; output.seek(SeekFrom::Current(size as i64))?; - Ok(()) } @@ -169,7 +205,7 @@ fn populate_blockfile(mut output: File, height: usize) -> io::Result<()> { let mut prev_block = genesis.clone(); for _ in 1..=height { - let tx = generate_transaction(height); + let tx = generate_coinbase_transaction(height); let hash: sha256d::Hash = tx.txid().into(); let merkle_root = hash.into(); let block = Block { @@ -181,9 +217,14 @@ fn populate_blockfile(mut output: File, height: usize) -> io::Result<()> { bits: 0, nonce: 0, }, - txdata: vec![tx], + txdata: vec![ + tx, + generate_spending_transaction(OutPoint { + txid: prev_block.txdata[0].txid(), + vout: 0, + }), + ], }; - serialize_block(&mut output, &block)?; prev_block = block; } diff --git a/tests/list.rs b/tests/list.rs new file mode 100644 index 0000000000..ad69ed7657 --- /dev/null +++ b/tests/list.rs @@ -0,0 +1,21 @@ +use super::*; + +#[test] +fn first_coinbase_transaction() -> Result { + Test::new()? + .command( + "list --blocksdir blocks 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0", + ) + .expected_stdout("[0,5000000000)\n") + .run() +} + +#[test] +fn second_coinbase_transaction() -> Result { + Test::new()? + .command( + "list --blocksdir blocks e5fb252959bdc7727c80296dbc53e1583121503bb2e266a609ebc49cf2a74c1d:0", + ) + .expected_stdout("[5000000000,10000000000)\n") + .run() +} diff --git a/tests/range.rs b/tests/range.rs index 39565bf9c4..f8370d45a9 100644 --- a/tests/range.rs +++ b/tests/range.rs @@ -39,3 +39,11 @@ fn genesis_names() -> Result { .expected_stdout("[nvtdijuwxlo,nvtcsezkbtg)\n") .run() } + +#[test] +fn names_before_last() -> Result { + Test::new()? + .args(&["range", "--name", "6929998"]) + .expected_stdout("[a,)\n") + .run() +}