diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index f30058f2a7..87e3510802 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -80,6 +80,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -163,6 +178,20 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-compression" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-dup" version = "1.2.2" @@ -380,6 +409,7 @@ dependencies = [ "bitflags", "bytes", "futures-util", + "headers", "http", "http-body", "hyper", @@ -569,12 +599,39 @@ dependencies = [ "syn", ] +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.3.0" @@ -741,6 +798,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-utils" version = "0.8.14" @@ -1082,6 +1148,16 @@ dependencies = [ "instant", ] +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1316,6 +1392,31 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1 0.10.5", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.0" @@ -1442,9 +1543,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.23" +version = "0.14.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" dependencies = [ "bytes", "futures-channel", @@ -1731,6 +1832,20 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mp4" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509348cba250e7b852a875100a2ddce7a36ee3abf881a681c756670c1774264d" +dependencies = [ + "byteorder", + "bytes", + "num-rational", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "never" version = "0.1.0" @@ -1790,6 +1905,19 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1847,11 +1975,12 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "ord" -version = "0.4.0" +version = "0.5.1" dependencies = [ "anyhow", "axum", "axum-server", + "base64 0.13.1", "bech32", "bip39", "bitcoin", @@ -1866,12 +1995,14 @@ dependencies = [ "hex", "html-escaper", "http", + "hyper", "indicatif", "lazy_static", "log", "mime", "mime_guess", "miniscript", + "mp4", "ord-bitcoincore-rpc", "pulldown-cmark", "redb", @@ -1882,6 +2013,7 @@ dependencies = [ "rustls-acme", "serde", "serde_json", + "serde_yaml", "sys-info", "tempfile", "tokio", @@ -1892,8 +2024,9 @@ dependencies = [ [[package]] name = "ord-bitcoincore-rpc" -version = "0.16.4" -source = "git+https://github.com/casey/rust-bitcoincore-rpc?branch=ord#1bc2638f5a3049495a7aa953c4c6dac44fd1524b" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca77a54d5a14a2731dd85150e24be89c849bdcb5704d23097188d763381d98b8" dependencies = [ "jsonrpc", "log", @@ -1904,8 +2037,9 @@ dependencies = [ [[package]] name = "ord-bitcoincore-rpc-json" -version = "0.16.4" -source = "git+https://github.com/casey/rust-bitcoincore-rpc?branch=ord#1bc2638f5a3049495a7aa953c4c6dac44fd1524b" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18b81ced8ed99f50d3f221a7de8297cbdf9bdf20ca2908a12f6128d38b2284fb" dependencies = [ "bitcoin", "serde", @@ -2061,9 +2195,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.17.3" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28fcd1e73f06ec85bf3280c48c67e731d8290ad3d730f8be9dc07946923005c8" +checksum = "75439f995d07ddfad42b192dfcf3bc66a7ecfd8b4a1f5f6f046aa5c2c5d7677d" dependencies = [ "once_cell", "target-lexicon", @@ -2179,8 +2313,9 @@ dependencies = [ [[package]] name = "redb" -version = "0.11.0" -source = "git+https://github.com/casey/redb.git?branch=ord#826fd633b2e26bb649cf7004406cb469fbb28837" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f210bb101d3a0ddba42f67b12a1d7186e584733ad028f119c8d217d867f03d" dependencies = [ "libc", "pyo3-build-config", @@ -2535,6 +2670,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82e6c8c047aa50a7328632d067bcae6ef38772a79e28daf32f735e0e4f3dd10" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.6.1" @@ -2544,6 +2692,17 @@ dependencies = [ "sha1_smol", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + [[package]] name = "sha1_smol" version = "1.0.0" @@ -2707,7 +2866,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "sha1", + "sha1 0.6.1", "syn", ] @@ -2987,6 +3146,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ + "async-compression", "bitflags", "bytes", "futures-core", @@ -2995,6 +3155,8 @@ dependencies = [ "http-body", "http-range-header", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", @@ -3097,6 +3259,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2024452afd3874bf539695e04af6732ba06517424dbf958fdb16a01f3bef6c" + [[package]] name = "untrusted" version = "0.7.1" @@ -3404,3 +3572,8 @@ checksum = "aed2e7a52e3744ab4d0c05c20aa065258e84c49fd4226f5191b2ed29712710b4" dependencies = [ "time 0.3.17", ] + +[[patch.unused]] +name = "redb" +version = "0.11.0" +source = "git+https://github.com/casey/redb.git?branch=ord#826fd633b2e26bb649cf7004406cb469fbb28837" diff --git a/src/index.rs b/src/index.rs index e4c02d853c..a113672b00 100644 --- a/src/index.rs +++ b/src/index.rs @@ -18,12 +18,12 @@ use { std::sync::atomic::{self, AtomicBool}, }; -mod entry; +pub(crate) mod entry; mod fetcher; mod rtx; mod updater; -const SCHEMA_VERSION: u64 = 3; +const SCHEMA_VERSION: u64 = 4; macro_rules! define_table { ($name:ident, $key:ty, $value:ty) => { @@ -568,7 +568,6 @@ impl Index { .open_table(SATPOINT_TO_INSCRIPTION_ID)?, outpoint, )? - .into_iter() .map(|(_satpoint, inscription_id)| inscription_id) .collect(), ) @@ -1032,7 +1031,7 @@ mod tests { let inscription = inscription("text/plain;charset=utf-8", "hello"); let template = TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription.to_witness(), + witnesses: vec![inscription.to_witness()], ..Default::default() }; @@ -1378,7 +1377,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -1403,7 +1402,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -1447,7 +1446,7 @@ mod tests { let first_txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); @@ -1455,7 +1454,7 @@ mod tests { let second_txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0)], - witness: inscription("text/png", [1; 100]).to_witness(), + witnesses: vec![inscription("text/png", [1; 100]).to_witness()], ..Default::default() }); let second_inscription_id = InscriptionId::from(second_txid); @@ -1502,7 +1501,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -1551,7 +1550,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -1595,7 +1594,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -1632,7 +1631,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], fee: 50 * COIN_VALUE, - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -1661,7 +1660,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], fee: 50 * COIN_VALUE, - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -1687,7 +1686,7 @@ mod tests { let first_txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], fee: 50 * COIN_VALUE, - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); let first_inscription_id = InscriptionId::from(first_txid); @@ -1698,7 +1697,7 @@ mod tests { let second_txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(3, 0, 0)], fee: 50 * COIN_VALUE, - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); let second_inscription_id = InscriptionId::from(second_txid); @@ -1812,7 +1811,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0)], outputs: 2, - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -1844,7 +1843,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], outputs: 2, - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], output_values: &[0, 50 * COIN_VALUE], ..Default::default() }); @@ -1870,7 +1869,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], fee: 50 * COIN_VALUE, - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -1979,7 +1978,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); @@ -2038,7 +2037,7 @@ mod tests { let first = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); @@ -2071,7 +2070,7 @@ mod tests { let second = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0)], - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); @@ -2110,7 +2109,7 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -2137,7 +2136,7 @@ mod tests { for i in 0..103 { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(i + 1, 0, 0)], - witness: inscription("text/plain", "hello").to_witness(), + witnesses: vec![inscription("text/plain", "hello").to_witness()], ..Default::default() }); ids.push(InscriptionId::from(txid)); @@ -2190,4 +2189,76 @@ mod tests { ); } } + + #[test] + fn test_inscription_with_parent() { + // for context in Context::configurations() { + let context = Context::builder().build(); + + context.mine_blocks(1); + + let parent_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witnesses: vec![inscription("text/plain", "parent").to_witness()], + ..Default::default() + }); + + let parent_id = InscriptionId::from(parent_txid); + + context.mine_blocks(1); + + assert_eq!( + context.index.get_inscription_entry(parent_id).unwrap(), + Some(InscriptionEntry { + fee: 0, + height: 2, + number: 0, + parent: None, + sat: None, + timestamp: 2 + }) + ); + + let child_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0), (2, 0, 0)], + witnesses: vec![ + Witness::new(), + inscription_with_parent("text/plain", "child", parent_id).to_witness(), + ], + ..Default::default() + }); + + let child_id = InscriptionId { + txid: child_txid, + index: 0, + }; + + context.mine_blocks(1); + + // parent is transferred successfully + context.index.assert_inscription_location( + parent_id, + SatPoint { + outpoint: OutPoint { + txid: child_txid, + vout: 0, + }, + offset: 0, + }, + 50 * COIN_VALUE, + ); + + // child inscription successfully added to database + assert_eq!( + context.index.get_inscription_entry(child_id).unwrap(), + Some(InscriptionEntry { + fee: 0, + height: 3, + number: 1, + parent: Some(parent_id), + sat: None, + timestamp: 3 + }) + ); + } } diff --git a/src/index/entry.rs b/src/index/entry.rs index 15ff3d8ecb..5a971c1af7 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -1,6 +1,6 @@ use super::*; -pub(super) trait Entry: Sized { +pub(crate) trait Entry: Sized { type Value; fn load(value: Self::Value) -> Self; @@ -22,24 +22,27 @@ impl Entry for BlockHash { } } +#[derive(Debug, PartialEq)] pub(crate) struct InscriptionEntry { pub(crate) fee: u64, pub(crate) height: u64, pub(crate) number: u64, + pub(crate) parent: Option, pub(crate) sat: Option, pub(crate) timestamp: u32, } -pub(crate) type InscriptionEntryValue = (u64, u64, u64, u64, u32); +pub(crate) type InscriptionEntryValue = (u64, u64, u64, (u128, u128, u32), u64, u32); impl Entry for InscriptionEntry { type Value = InscriptionEntryValue; - fn load((fee, height, number, sat, timestamp): InscriptionEntryValue) -> Self { + fn load((fee, height, number, parent, sat, timestamp): InscriptionEntryValue) -> Self { Self { fee, height, number, + parent: Entry::load(parent), sat: if sat == u64::MAX { None } else { @@ -54,6 +57,7 @@ impl Entry for InscriptionEntry { self.fee, self.height, self.number, + self.parent.store(), match self.sat { Some(sat) => sat.n(), None => u64::MAX, @@ -85,6 +89,74 @@ impl Entry for InscriptionId { } } +type ParentValue = (u128, u128, u32); + +impl Entry for Option { + type Value = ParentValue; + + fn load(value: Self::Value) -> Self { + if (0, 0, u32::MAX) == value { + None + } else { + let (head, tail, index) = value; + let head_array = head.to_le_bytes(); + let tail_array = tail.to_le_bytes(); + let index_array = index.to_be_bytes(); + let array = [ + head_array[0], + head_array[1], + head_array[2], + head_array[3], + head_array[4], + head_array[5], + head_array[6], + head_array[7], + head_array[8], + head_array[9], + head_array[10], + head_array[11], + head_array[12], + head_array[13], + head_array[14], + head_array[15], + tail_array[0], + tail_array[1], + tail_array[2], + tail_array[3], + tail_array[4], + tail_array[5], + tail_array[6], + tail_array[7], + tail_array[8], + tail_array[9], + tail_array[10], + tail_array[11], + tail_array[12], + tail_array[13], + tail_array[14], + tail_array[15], + index_array[0], + index_array[1], + index_array[2], + index_array[3], + ]; + + Some(InscriptionId::load(array)) + } + } + // TODO: test head and tail byte order + fn store(self) -> Self::Value { + if let Some(inscription_id) = self { + let txid_entry = inscription_id.txid.store(); + let little_end = u128::from_le_bytes(txid_entry[..16].try_into().unwrap()); + let big_end = u128::from_le_bytes(txid_entry[16..].try_into().unwrap()); + (little_end, big_end, inscription_id.index) + } else { + (0, 0, u32::MAX) + } + } +} + pub(super) type OutPointValue = [u8; 36]; impl Entry for OutPoint { @@ -143,3 +215,100 @@ impl Entry for SatRange { n.to_le_bytes()[0..11].try_into().unwrap() } } + +pub(super) type TxidValue = [u8; 32]; + +impl Entry for Txid { + type Value = TxidValue; + + fn load(value: Self::Value) -> Self { + Txid::from_inner(value) + } + + fn store(self) -> Self::Value { + Txid::into_inner(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parent_entry() { + let inscription_id: Option = None; + + assert_eq!(inscription_id.store(), (0, 0, u32::MAX)); + assert_eq!( + as Entry>::load((0, 0, u32::MAX)), + inscription_id + ); + + let inscription_id = Some( + "0000000000000000000000000000000000000000000000000000000000000000i0" + .parse::() + .unwrap(), + ); + + assert_eq!(inscription_id.store(), (0, 0, 0)); + assert_eq!( + as Entry>::load((0, 0, 0)), + inscription_id + ); + + let inscription_id = Some( + "ffffffffffffffffffffffffffffffff00000000000000000000000000000000i0" + .parse::() + .unwrap(), + ); + + assert_eq!(inscription_id.store(), (0, u128::MAX, 0)); + assert_eq!( + as Entry>::load((0, u128::MAX, 0)), + inscription_id + ); + } + + #[test] + fn parent_entry_individual_byte_order() { + let inscription_id = Some( + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefi0" + .parse::() + .unwrap(), + ); + + assert_eq!( + inscription_id.store(), + ( + 0x0123456789abcdef0123456789abcdef, + 0x0123456789abcdef0123456789abcdef, + 0 + ) + ); + + assert_eq!( + as Entry>::load(( + 0x0123456789abcdef0123456789abcdef, + 0x0123456789abcdef0123456789abcdef, + 0 + )), + inscription_id + ); + } + + #[test] + fn parent_entry_index() { + let inscription_id = Some( + "0000000000000000000000000000000000000000000000000000000000000000i1" + .parse::() + .unwrap(), + ); + + assert_eq!(inscription_id.store(), (0, 0, 1)); + + assert_eq!( + as Entry>::load((0, 0, 1)), + inscription_id + ); + } +} diff --git a/src/index/updater.rs b/src/index/updater.rs index 5d56e4e9ab..9ce9292b95 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -65,6 +65,7 @@ impl Updater { range_cache: HashMap::new(), height, index_sats: index.has_sat_index()?, + sat_ranges_since_flush: 0, outputs_cached: 0, outputs_inserted_since_flush: 0, @@ -523,6 +524,7 @@ impl Updater { outpoint_to_sat_ranges.insert(&OutPoint::null().store(), lost_sat_ranges.as_slice())?; } } else { + // move coinbase to end for (tx, txid) in block.txdata.iter().skip(1).chain(block.txdata.first()) { lost_sats += inscription_updater.index_transaction_inscriptions(tx, *txid, None)?; } diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 575ccf7ca7..9d8a6216cc 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -1,13 +1,16 @@ -use super::*; +use {super::*, std::collections::BTreeSet}; +#[derive(Clone, Copy, Debug)] pub(super) struct Flotsam { inscription_id: InscriptionId, offset: u64, origin: Origin, } +// change name to Jetsam or more poetic german word +#[derive(Clone, Copy, Debug)] enum Origin { - New(u64), + New((u64, Option)), Old(SatPoint), } @@ -73,50 +76,92 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { txid: Txid, input_sat_ranges: Option<&VecDeque<(u64, u64)>>, ) -> Result { - let mut inscriptions = Vec::new(); - + let mut floating_inscriptions = Vec::new(); + let mut inscribed_offsets = BTreeSet::new(); let mut input_value = 0; for tx_in in &tx.input { + // skip subsidy since no inscriptions possible if tx_in.previous_output.is_null() { input_value += Height(self.height).subsidy(); - } else { - for (old_satpoint, inscription_id) in - Index::inscriptions_on_output(self.satpoint_to_id, tx_in.previous_output)? - { - inscriptions.push(Flotsam { - offset: input_value + old_satpoint.offset, - inscription_id, - origin: Origin::Old(old_satpoint), + continue; + } + + // find existing inscriptions on input aka transfers + for (old_satpoint, inscription_id) in + Index::inscriptions_on_output(self.satpoint_to_id, tx_in.previous_output)? + { + floating_inscriptions.push(Flotsam { + offset: input_value + old_satpoint.offset, + inscription_id, + origin: Origin::Old(old_satpoint), + }); + + inscribed_offsets.insert(input_value + old_satpoint.offset); + } + + // find new inscriptions + if let Some(inscription) = Inscription::from_tx_input(tx_in) { + // ignore new inscriptions on already inscribed offset (sats) + if !inscribed_offsets.contains(&input_value) { + // parent has to be in an input before child + // think about specifying a more general approach in a protocol doc/BIP + let parent = inscription.get_parent_id().filter(|&parent_id| { + floating_inscriptions + .iter() + .any(|flotsam| flotsam.inscription_id == parent_id) }); - } - input_value += if let Some(value) = self.value_cache.remove(&tx_in.previous_output) { - value - } else if let Some(value) = self - .outpoint_to_value - .remove(&tx_in.previous_output.store())? - { - value.value() - } else { - self.value_receiver.blocking_recv().ok_or_else(|| { - anyhow!( - "failed to get transaction for {}", - tx_in.previous_output.txid - ) - })? + floating_inscriptions.push(Flotsam { + inscription_id: InscriptionId { + txid, + index: 0, // will have to be updated for multi inscriptions + }, + offset: input_value, + origin: Origin::New((0, parent)), + }); } } + + // different ways to get the utxo set (input amount) + input_value += if let Some(value) = self.value_cache.remove(&tx_in.previous_output) { + value + } else if let Some(value) = self + .outpoint_to_value + .remove(&tx_in.previous_output.store())? + { + value.value() + } else { + self.value_receiver.blocking_recv().ok_or_else(|| { + anyhow!( + "failed to get transaction for {}", + tx_in.previous_output.txid + ) + })? + } } - if inscriptions.iter().all(|flotsam| flotsam.offset != 0) - && Inscription::from_transaction(tx).is_some() - { - inscriptions.push(Flotsam { - inscription_id: txid.into(), - offset: 0, - origin: Origin::New(input_value - tx.output.iter().map(|txout| txout.value).sum::()), - }); - }; + // TODO: inefficient + // calulate genesis fee for new inscriptions + let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); + let mut floating_inscriptions = floating_inscriptions + .into_iter() + .map(|flotsam| { + if let Flotsam { + inscription_id, + offset, + origin: Origin::New((_, parent)), + } = flotsam + { + Flotsam { + inscription_id, + offset, + origin: Origin::New((input_value - total_output_value, parent)), + } + } else { + flotsam + } + }) + .collect::>(); let is_coinbase = tx .input @@ -125,11 +170,11 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { .unwrap_or_default(); if is_coinbase { - inscriptions.append(&mut self.flotsam); + floating_inscriptions.append(&mut self.flotsam); } - inscriptions.sort_by_key(|flotsam| flotsam.offset); - let mut inscriptions = inscriptions.into_iter().peekable(); + floating_inscriptions.sort_by_key(|flotsam| flotsam.offset); + let mut inscriptions = floating_inscriptions.into_iter().peekable(); let mut output_value = 0; for (vout, tx_out) in tx.output.iter().enumerate() { @@ -150,7 +195,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { self.update_inscription_location( input_sat_ranges, - inscriptions.next().unwrap(), + inscriptions.next().unwrap(), // TODO: do something with two inscriptions in the input new_satpoint, )?; } @@ -198,7 +243,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { Origin::Old(old_satpoint) => { self.satpoint_to_id.remove(&old_satpoint.store())?; } - Origin::New(fee) => { + Origin::New((fee, parent)) => { self .number_to_id .insert(&self.next_number, &inscription_id)?; @@ -224,6 +269,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { fee, height: self.height, number: self.next_number, + parent, sat, timestamp: self.timestamp, } diff --git a/src/inscription.rs b/src/inscription.rs index d0fba77016..037cc24dca 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -1,5 +1,6 @@ use { super::*, + crate::index::entry::Entry, bitcoin::{ blockdata::{ opcodes, @@ -15,24 +16,48 @@ const PROTOCOL_ID: &[u8] = b"ord"; const BODY_TAG: &[u8] = &[]; const CONTENT_TYPE_TAG: &[u8] = &[1]; +const PARENT_TAG: &[u8] = &[3]; #[derive(Debug, PartialEq, Clone)] pub(crate) struct Inscription { - body: Option>, + parent: Option, content_type: Option>, + body: Option>, } impl Inscription { #[cfg(test)] - pub(crate) fn new(content_type: Option>, body: Option>) -> Self { - Self { content_type, body } + pub(crate) fn new( + parent: Option, + content_type: Option>, + body: Option>, + ) -> Self { + Self { + parent, + content_type, + body, + } } pub(crate) fn from_transaction(tx: &Transaction) -> Option { - InscriptionParser::parse(&tx.input.get(0)?.witness).ok() + for input in &tx.input { + if let Some(inscription) = Inscription::from_tx_input(input) { + return Some(inscription); + } + } + + None } - pub(crate) fn from_file(chain: Chain, path: impl AsRef) -> Result { + pub(crate) fn from_tx_input(tx_in: &TxIn) -> Option { + InscriptionParser::parse(&tx_in.witness).ok() + } + + pub(crate) fn from_file( + chain: Chain, + path: impl AsRef, + parent: Option, + ) -> Result { let path = path.as_ref(); let body = fs::read(path).with_context(|| format!("io error reading {}", path.display()))?; @@ -47,6 +72,7 @@ impl Inscription { let content_type = Media::content_type_for_path(path)?; Ok(Self { + parent, body: Some(body), content_type: Some(content_type.into()), }) @@ -58,6 +84,10 @@ impl Inscription { .push_opcode(opcodes::all::OP_IF) .push_slice(PROTOCOL_ID); + if let Some(parent) = &self.parent { + builder = builder.push_slice(PARENT_TAG).push_slice(&parent.store()); + } + if let Some(content_type) = &self.content_type { builder = builder .push_slice(CONTENT_TYPE_TAG) @@ -106,6 +136,10 @@ impl Inscription { str::from_utf8(self.content_type.as_ref()?).ok() } + pub(crate) fn get_parent_id(&self) -> Option { + self.parent + } + #[cfg(test)] pub(crate) fn to_witness(&self) -> Witness { let builder = script::Builder::new(); @@ -222,6 +256,7 @@ impl<'a> InscriptionParser<'a> { let body = fields.remove(BODY_TAG); let content_type = fields.remove(CONTENT_TYPE_TAG); + let parent = fields.remove(PARENT_TAG); for tag in fields.keys() { if let Some(lsb) = tag.first() { @@ -231,7 +266,12 @@ impl<'a> InscriptionParser<'a> { } } - return Ok(Some(Inscription { body, content_type })); + return Ok(Some(Inscription { + body, + content_type, + parent: parent + .and_then(|parent| Some(InscriptionId::load(parent.as_slice().try_into().ok()?))), + })); } Ok(None) @@ -358,7 +398,7 @@ mod tests { b"ord", &[1], b"text/plain;charset=utf-8", - &[3], + &[5], b"bar", &[], b"ord", @@ -372,6 +412,7 @@ mod tests { assert_eq!( InscriptionParser::parse(&envelope(&[b"ord", &[1], b"text/plain;charset=utf-8"])), Ok(Inscription { + parent: None, content_type: Some(b"text/plain;charset=utf-8".to_vec()), body: None, }), @@ -383,6 +424,7 @@ mod tests { assert_eq!( InscriptionParser::parse(&envelope(&[b"ord", &[], b"foo"])), Ok(Inscription { + parent: None, content_type: None, body: Some(b"foo".to_vec()), }), @@ -576,6 +618,7 @@ mod tests { } #[test] + #[ignore] // we need to do this now for parent-child relationships fn do_not_extract_from_second_input() { let tx = Transaction { version: 0, @@ -705,6 +748,7 @@ mod tests { witness.push( &Inscription { + parent: None, content_type: None, body: None, } @@ -716,6 +760,7 @@ mod tests { assert_eq!( InscriptionParser::parse(&witness).unwrap(), Inscription { + parent: None, content_type: None, body: None, } @@ -725,8 +770,9 @@ mod tests { #[test] fn unknown_odd_fields_are_ignored() { assert_eq!( - InscriptionParser::parse(&envelope(&[b"ord", &[3], &[0]])), + InscriptionParser::parse(&envelope(&[b"ord", &[5], &[0]])), Ok(Inscription { + parent: None, content_type: None, body: None, }), diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index d9f402e47e..13ba8d1730 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -86,6 +86,7 @@ impl Preview { dry_run: false, no_limit: false, destination: None, + parent: None, }, )), } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 63adafb300..684862afc5 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -768,7 +768,7 @@ impl Server { .get_inscription_by_id(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - return match inscription.media() { + match inscription.media() { Media::Audio => Ok(PreviewAudioHtml { inscription_id }.into_response()), Media::Iframe => Ok( Self::content_response(inscription) @@ -809,7 +809,7 @@ impl Server { } Media::Unknown => Ok(PreviewUnknownHtml.into_response()), Media::Video => Ok(PreviewVideoHtml { inscription_id }.into_response()), - }; + } } async fn inscription( @@ -859,6 +859,7 @@ impl Server { next, number: entry.number, output, + parent: entry.parent, previous, sat: entry.sat, satpoint, @@ -1971,6 +1972,7 @@ mod tests { fn content_response_no_content() { assert_eq!( Server::content_response(Inscription::new( + None, Some("text/plain".as_bytes().to_vec()), None )), @@ -1981,6 +1983,7 @@ mod tests { #[test] fn content_response_with_content() { let (headers, body) = Server::content_response(Inscription::new( + None, Some("text/plain".as_bytes().to_vec()), Some(vec![1, 2, 3]), )) @@ -1993,7 +1996,7 @@ mod tests { #[test] fn content_response_no_content_type() { let (headers, body) = - Server::content_response(Inscription::new(None, Some(Vec::new()))).unwrap(); + Server::content_response(Inscription::new(None, None, Some(Vec::new()))).unwrap(); assert_eq!(headers["content-type"], "application/octet-stream"); assert!(body.is_empty()); @@ -2006,7 +2009,7 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/plain;charset=utf-8", "hello").to_witness(), + witnesses: vec![inscription("text/plain;charset=utf-8", "hello").to_witness()], ..Default::default() }); @@ -2027,7 +2030,7 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/plain;charset=utf-8", b"\xc3\x28").to_witness(), + witnesses: vec![inscription("text/plain;charset=utf-8", b"\xc3\x28").to_witness()], ..Default::default() }); @@ -2047,11 +2050,11 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription( + witnesses: vec![inscription( "text/plain;charset=utf-8", "", ) - .to_witness(), + .to_witness()], ..Default::default() }); @@ -2072,7 +2075,7 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("audio/flac", "hello").to_witness(), + witnesses: vec![inscription("audio/flac", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -2093,7 +2096,7 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("application/pdf", "hello").to_witness(), + witnesses: vec![inscription("application/pdf", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -2114,7 +2117,7 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("image/png", "hello").to_witness(), + witnesses: vec![inscription("image/png", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -2136,7 +2139,7 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/html;charset=utf-8", "hello").to_witness(), + witnesses: vec![inscription("text/html;charset=utf-8", "hello").to_witness()], ..Default::default() }); @@ -2157,7 +2160,7 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/foo", "hello").to_witness(), + witnesses: vec![inscription("text/foo", "hello").to_witness()], ..Default::default() }); @@ -2178,7 +2181,7 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("video/webm", "hello").to_witness(), + witnesses: vec![inscription("video/webm", "hello").to_witness()], ..Default::default() }); let inscription_id = InscriptionId::from(txid); @@ -2199,7 +2202,7 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/foo", "hello").to_witness(), + witnesses: vec![inscription("text/foo", "hello").to_witness()], ..Default::default() }); @@ -2219,7 +2222,7 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/foo", "hello").to_witness(), + witnesses: vec![inscription("text/foo", "hello").to_witness()], ..Default::default() }); @@ -2239,7 +2242,7 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/foo", "hello").to_witness(), + witnesses: vec![inscription("text/foo", "hello").to_witness()], ..Default::default() }); @@ -2271,7 +2274,7 @@ mod tests { server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/foo", "hello").to_witness(), + witnesses: vec![inscription("text/foo", "hello").to_witness()], ..Default::default() }); @@ -2291,7 +2294,9 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: Inscription::new(Some("foo/bar".as_bytes().to_vec()), None).to_witness(), + witnesses: vec![ + Inscription::new(None, Some("foo/bar".as_bytes().to_vec()), None).to_witness(), + ], ..Default::default() }); @@ -2313,7 +2318,9 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: Inscription::new(Some("image/png".as_bytes().to_vec()), None).to_witness(), + witnesses: vec![ + Inscription::new(None, Some("image/png".as_bytes().to_vec()), None).to_witness(), + ], ..Default::default() }); @@ -2335,7 +2342,7 @@ mod tests { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/foo", "hello").to_witness(), + witnesses: vec![inscription("text/foo", "hello").to_witness()], ..Default::default() }); @@ -2367,7 +2374,7 @@ mod tests { server.mine_blocks(1); server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(i + 1, 0, 0)], - witness: inscription("text/foo", "hello").to_witness(), + witnesses: vec![inscription("text/foo", "hello").to_witness()], ..Default::default() }); } @@ -2389,7 +2396,7 @@ mod tests { server.mine_blocks(1); server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(i + 1, 0, 0)], - witness: inscription("text/foo", "hello").to_witness(), + witnesses: vec![inscription("text/foo", "hello").to_witness()], ..Default::default() }); } @@ -2453,7 +2460,7 @@ mod tests { bitcoin_rpc_server.mine_blocks(1); let txid = bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0)], - witness: inscription("text/plain;charset=utf-8", "hello").to_witness(), + witnesses: vec![inscription("text/plain;charset=utf-8", "hello").to_witness()], ..Default::default() }); let inscription = InscriptionId::from(txid); diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index d9b537f82d..ca74c36cf0 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -1,3 +1,5 @@ +use bitcoin::SchnorrSig; + use { super::*, crate::wallet::Wallet, @@ -22,6 +24,7 @@ use { struct Output { commit: Txid, inscription: InscriptionId, + parent: Option, reveal: Txid, fees: u64, } @@ -54,12 +57,12 @@ pub(crate) struct Inscribe { pub(crate) dry_run: bool, #[clap(long, help = "Send inscription to .")] pub(crate) destination: Option
, + #[clap(long, help = "Establish parent relationship with .")] + pub(crate) parent: Option, } impl Inscribe { pub(crate) fn run(self, options: Options) -> Result { - let inscription = Inscription::from_file(options.chain(), &self.file)?; - let index = Index::open(&options)?; index.update()?; @@ -69,6 +72,34 @@ impl Inscribe { let inscriptions = index.get_inscriptions(None)?; + let (parent, commit_input_offset) = if let Some(parent_id) = self.parent { + if let Some(satpoint) = index.get_inscription_satpoint_by_id(parent_id)? { + if !utxos.contains_key(&satpoint.outpoint) { + return Err(anyhow!(format!( + "unrelated parent {parent_id} not accepting mailman's child" // for the germans: "Kuckuckskind" + ))); + } + + let output = index + .get_transaction(satpoint.outpoint.txid)? + .expect("not found") + .output + .into_iter() + .nth(satpoint.outpoint.vout.try_into().unwrap()) + .expect("current transaction output"); + + (Some((satpoint, output)), 1) + } else { + return Err(anyhow!(format!( + "specified parent {parent_id} does not exist" + ))); + } + } else { + (None, 0) + }; + + let inscription = Inscription::from_file(options.chain(), &self.file, self.parent)?; + let commit_tx_change = [get_change_address(&client)?, get_change_address(&client)?]; let reveal_tx_destination = self @@ -76,9 +107,10 @@ impl Inscribe { .map(Ok) .unwrap_or_else(|| get_change_address(&client))?; - let (unsigned_commit_tx, reveal_tx, recovery_key_pair) = + let (unsigned_commit_tx, partially_signed_reveal_tx, recovery_key_pair) = Inscribe::create_inscription_transactions( self.satpoint, + parent, inscription, inscriptions, options.chain().network(), @@ -91,47 +123,69 @@ impl Inscribe { )?; utxos.insert( - reveal_tx.input[0].previous_output, + partially_signed_reveal_tx.input[commit_input_offset].previous_output, Amount::from_sat( - unsigned_commit_tx.output[reveal_tx.input[0].previous_output.vout as usize].value, + unsigned_commit_tx.output[partially_signed_reveal_tx.input[commit_input_offset] + .previous_output + .vout as usize] + .value, ), ); - let fees = - Self::calculate_fee(&unsigned_commit_tx, &utxos) + Self::calculate_fee(&reveal_tx, &utxos); + let fees = Self::calculate_fee(&unsigned_commit_tx, &utxos) + + Self::calculate_fee(&partially_signed_reveal_tx, &utxos); if self.dry_run { print_json(Output { commit: unsigned_commit_tx.txid(), - reveal: reveal_tx.txid(), - inscription: reveal_tx.txid().into(), + reveal: partially_signed_reveal_tx.txid(), + inscription: partially_signed_reveal_tx.txid().into(), + parent: self.parent, fees, })?; - } else { - if !self.no_backup { - Inscribe::backup_recovery_key(&client, recovery_key_pair, options.chain().network())?; - } - let signed_raw_commit_tx = client - .sign_raw_transaction_with_wallet(&unsigned_commit_tx, None, None)? - .hex; + return Ok(()); + } - let commit = client - .send_raw_transaction(&signed_raw_commit_tx) - .context("Failed to send commit transaction")?; + if !self.no_backup { + Inscribe::backup_recovery_key(&client, recovery_key_pair, options.chain().network())?; + } - let reveal = client - .send_raw_transaction(&reveal_tx) - .context("Failed to send reveal transaction")?; + let signed_raw_commit_tx = client + .sign_raw_transaction_with_wallet(&unsigned_commit_tx, None, None)? + .hex; - print_json(Output { - commit, - reveal, - inscription: reveal.into(), - fees, - })?; + let commit = client + .send_raw_transaction(&signed_raw_commit_tx) + .context("Failed to send commit transaction")?; + + let reveal = if self.parent.is_some() { + let fully_signed_raw_reveal_tx = client + .sign_raw_transaction_with_wallet(&partially_signed_reveal_tx, None, None)? + .hex; + + client + .send_raw_transaction(&fully_signed_raw_reveal_tx) + .context("Failed to send reveal transaction")? + } else { + client + .send_raw_transaction(&partially_signed_reveal_tx) + .context("Failed to send reveal transaction")? + }; + + let inscription = InscriptionId { + txid: reveal, + index: 0, }; + print_json(Output { + commit, + reveal, + inscription, + parent: self.parent, + fees, + })?; + Ok(()) } @@ -146,6 +200,7 @@ impl Inscribe { fn create_inscription_transactions( satpoint: Option, + parent: Option<(SatPoint, TxOut)>, inscription: Inscription, inscriptions: BTreeMap, network: Network, @@ -209,17 +264,42 @@ impl Inscribe { let commit_tx_address = Address::p2tr_tweaked(taproot_spend_info.output_key(), network); + let (mut inputs, mut outputs, commit_input_offset) = + if let Some((satpoint, output)) = parent.clone() { + ( + vec![satpoint.outpoint, OutPoint::null()], + vec![ + TxOut { + script_pubkey: output.script_pubkey, + value: output.value, + }, + TxOut { + script_pubkey: destination.script_pubkey(), + value: 0, + }, + ], + 1, + ) + } else { + ( + vec![OutPoint::null()], + vec![TxOut { + script_pubkey: destination.script_pubkey(), + value: 0, + }], + 0, + ) + }; + let (_, reveal_fee) = Self::build_reveal_transaction( &control_block, reveal_fee_rate, - OutPoint::null(), - TxOut { - script_pubkey: destination.script_pubkey(), - value: 0, - }, + inputs.clone(), + outputs.clone(), &reveal_script, ); + // watch out that parent and inscription preserved let unsigned_commit_tx = TransactionBuilder::build_transaction_with_value( satpoint, inscriptions, @@ -237,50 +317,76 @@ impl Inscribe { .find(|(_vout, output)| output.script_pubkey == commit_tx_address.script_pubkey()) .expect("should find sat commit/inscription output"); + inputs[commit_input_offset] = OutPoint { + txid: unsigned_commit_tx.txid(), + vout: vout.try_into().unwrap(), + }; + + outputs[commit_input_offset] = TxOut { + script_pubkey: destination.script_pubkey(), + value: output.value, + }; + let (mut reveal_tx, fee) = Self::build_reveal_transaction( &control_block, reveal_fee_rate, - OutPoint { - txid: unsigned_commit_tx.txid(), - vout: vout.try_into().unwrap(), - }, - TxOut { - script_pubkey: destination.script_pubkey(), - value: output.value, - }, + inputs, + outputs, &reveal_script, ); - reveal_tx.output[0].value = reveal_tx.output[0] + reveal_tx.output[commit_input_offset].value = reveal_tx.output[commit_input_offset] .value .checked_sub(fee.to_sat()) .context("commit transaction output value insufficient to pay transaction fee")?; - if reveal_tx.output[0].value < reveal_tx.output[0].script_pubkey.dust_value().to_sat() { + if reveal_tx.output[commit_input_offset].value + < reveal_tx.output[commit_input_offset] + .script_pubkey + .dust_value() + .to_sat() + { bail!("commit transaction output would be dust"); } + // NB. This binding is to avoid borrow-checker problems + let prevouts_all_inputs = &[output]; + + let (prevouts, hash_ty) = if parent.is_some() { + ( + Prevouts::One(commit_input_offset, output), + SchnorrSighashType::AllPlusAnyoneCanPay, + ) + } else { + ( + Prevouts::All(prevouts_all_inputs), + SchnorrSighashType::Default, + ) + }; + let mut sighash_cache = SighashCache::new(&mut reveal_tx); - let signature_hash = sighash_cache + let message = sighash_cache .taproot_script_spend_signature_hash( - 0, - &Prevouts::All(&[output]), + commit_input_offset, + &prevouts, TapLeafHash::from_script(&reveal_script, LeafVersion::TapScript), - SchnorrSighashType::Default, + hash_ty, ) .expect("signature hash should compute"); - let signature = secp256k1.sign_schnorr( - &secp256k1::Message::from_slice(signature_hash.as_inner()) + let sig = secp256k1.sign_schnorr( + &secp256k1::Message::from_slice(message.as_inner()) .expect("should be cryptographically secure hash"), &key_pair, ); let witness = sighash_cache - .witness_mut(0) + .witness_mut(commit_input_offset) .expect("getting mutable witness reference should work"); - witness.push(signature.as_ref()); + + witness.push(SchnorrSig { sig, hash_ty }.to_vec()); + witness.push(reveal_script); witness.push(&control_block.serialize()); @@ -337,18 +443,21 @@ impl Inscribe { fn build_reveal_transaction( control_block: &ControlBlock, fee_rate: FeeRate, - input: OutPoint, - output: TxOut, + inputs: Vec, + outputs: Vec, script: &Script, ) -> (Transaction, Amount) { let reveal_tx = Transaction { - input: vec![TxIn { - previous_output: input, - script_sig: script::Builder::new().into_script(), - witness: Witness::new(), - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - }], - output: vec![output], + input: inputs + .iter() + .map(|outpoint| TxIn { + previous_output: *outpoint, + script_sig: script::Builder::new().into_script(), + witness: Witness::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + }) + .collect(), + output: outputs, lock_time: PackedLockTime::ZERO, version: 1, }; @@ -356,13 +465,15 @@ impl Inscribe { let fee = { let mut reveal_tx = reveal_tx.clone(); - reveal_tx.input[0].witness.push( - Signature::from_slice(&[0; SCHNORR_SIGNATURE_SIZE]) - .unwrap() - .as_ref(), - ); - reveal_tx.input[0].witness.push(script); - reveal_tx.input[0].witness.push(&control_block.serialize()); + for txin in &mut reveal_tx.input { + txin.witness.push( + Signature::from_slice(&[0; SCHNORR_SIGNATURE_SIZE]) + .unwrap() + .as_ref(), + ); + txin.witness.push(script); + txin.witness.push(&control_block.serialize()); + } fee_rate.fee(reveal_tx.vsize()) }; @@ -384,6 +495,7 @@ mod tests { let (commit_tx, reveal_tx, _private_key) = Inscribe::create_inscription_transactions( Some(satpoint(1, 0)), + None, inscription, BTreeMap::new(), Network::Bitcoin, @@ -415,6 +527,7 @@ mod tests { let (commit_tx, reveal_tx, _) = Inscribe::create_inscription_transactions( Some(satpoint(1, 0)), + None, inscription, BTreeMap::new(), Network::Bitcoin, @@ -450,6 +563,7 @@ mod tests { let error = Inscribe::create_inscription_transactions( satpoint, + None, inscription, inscriptions, Network::Bitcoin, @@ -492,6 +606,7 @@ mod tests { assert!(Inscribe::create_inscription_transactions( satpoint, + None, inscription, inscriptions, Network::Bitcoin, @@ -528,6 +643,7 @@ mod tests { let (commit_tx, reveal_tx, _private_key) = Inscribe::create_inscription_transactions( satpoint, + None, inscription, inscriptions, bitcoin::Network::Signet, @@ -590,6 +706,7 @@ mod tests { let (commit_tx, reveal_tx, _private_key) = Inscribe::create_inscription_transactions( satpoint, + None, inscription, inscriptions, bitcoin::Network::Signet, @@ -639,6 +756,7 @@ mod tests { let error = Inscribe::create_inscription_transactions( satpoint, + None, inscription, BTreeMap::new(), Network::Bitcoin, @@ -670,6 +788,7 @@ mod tests { let (_commit_tx, reveal_tx, _private_key) = Inscribe::create_inscription_transactions( satpoint, + None, inscription, BTreeMap::new(), Network::Bitcoin, diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index aed340be39..11f73d4f65 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -427,7 +427,6 @@ impl TransactionBuilder { version: 1, lock_time: PackedLockTime::ZERO, input: (0..inputs) - .into_iter() .map(|_| TxIn { previous_output: OutPoint::null(), script_sig: Script::new(), diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index 0f396903fe..c6c40dcfa9 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -10,6 +10,7 @@ pub(crate) struct InscriptionHtml { pub(crate) next: Option, pub(crate) number: u64, pub(crate) output: TxOut, + pub(crate) parent: Option, pub(crate) previous: Option, pub(crate) sat: Option, pub(crate) satpoint: SatPoint, @@ -42,6 +43,7 @@ mod tests { next: None, number: 1, output: tx_out(1, address()), + parent: None, previous: None, sat: None, satpoint: satpoint(1, 0), @@ -102,6 +104,7 @@ mod tests { number: 1, output: tx_out(1, address()), previous: None, + parent: None, sat: Some(Sat(1)), satpoint: satpoint(1, 0), timestamp: timestamp(0), @@ -133,6 +136,7 @@ mod tests { next: Some(inscription_id(3)), number: 1, output: tx_out(1, address()), + parent: None, previous: Some(inscription_id(1)), sat: None, satpoint: satpoint(1, 0), diff --git a/src/test.rs b/src/test.rs index 27a8d45f83..59df4a06fb 100644 --- a/src/test.rs +++ b/src/test.rs @@ -101,7 +101,19 @@ pub(crate) fn tx_out(value: u64, address: Address) -> TxOut { } pub(crate) fn inscription(content_type: &str, body: impl AsRef<[u8]>) -> Inscription { - Inscription::new(Some(content_type.into()), Some(body.as_ref().into())) + Inscription::new(None, Some(content_type.into()), Some(body.as_ref().into())) +} + +pub(crate) fn inscription_with_parent( + content_type: &str, + body: impl AsRef<[u8]>, + parent: InscriptionId, +) -> Inscription { + Inscription::new( + Some(parent), + Some(content_type.into()), + Some(body.as_ref().into()), + ) } pub(crate) fn inscription_id(n: u32) -> InscriptionId { diff --git a/templates/inscription.html b/templates/inscription.html index 37573f3706..fd1144a6ca 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -15,6 +15,10 @@

Inscription {{ self.number }}

id
{{ self.inscription_id }}
+%% if let Some(parent) = self.parent { +
parent
+
{{parent}}
+%% } %% if let Ok(address) = self.chain.address_from_script(&self.output.script_pubkey ) {
address
{{ address }}
diff --git a/test-bitcoincore-rpc/src/lib.rs b/test-bitcoincore-rpc/src/lib.rs index 7051d283d6..d7db02d5a0 100644 --- a/test-bitcoincore-rpc/src/lib.rs +++ b/test-bitcoincore-rpc/src/lib.rs @@ -118,7 +118,7 @@ pub struct TransactionTemplate<'a> { pub inputs: &'a [(usize, usize, usize)], pub output_values: &'a [u64], pub outputs: usize, - pub witness: Witness, + pub witnesses: Vec, } #[derive(Clone, Debug, PartialEq)] @@ -150,7 +150,7 @@ impl<'a> Default for TransactionTemplate<'a> { inputs: &[], output_values: &[], outputs: 1, - witness: Witness::default(), + witnesses: vec![], } } } diff --git a/test-bitcoincore-rpc/src/state.rs b/test-bitcoincore-rpc/src/state.rs index 80f887c003..08ea2a09c4 100644 --- a/test-bitcoincore-rpc/src/state.rs +++ b/test-bitcoincore-rpc/src/state.rs @@ -138,11 +138,10 @@ impl State { previous_output: OutPoint::new(tx.txid(), *vout as u32), script_sig: Script::new(), sequence: Sequence::MAX, - witness: if i == 0 { - template.witness.clone() - } else { - Witness::new() - }, + witness: template + .witnesses + .get(i) + .map_or(Witness::new(), |i| i.clone()), }); } diff --git a/tests/core.rs b/tests/core.rs index 8e4c951d38..4154574b82 100644 --- a/tests/core.rs +++ b/tests/core.rs @@ -1,4 +1,9 @@ -use super::*; +use { + super::*, + bitcoin::Address, + bitcoincore_rpc::{Client, RpcApi}, + std::ffi::OsString, +}; struct KillOnDrop(std::process::Child); @@ -70,3 +75,230 @@ fn preview() { format!(".*( u16 { + TcpListener::bind("127.0.0.1:0") + .unwrap() + .local_addr() + .unwrap() + .port() +} + +fn ord( + cookiefile: &std::path::Path, + ord_data_dir: &std::path::Path, + rpc_port: u16, + args: &[&str], +) -> Result { + let mut ord = Command::new(executable_path("ord")); + + ord + .env("ORD_INTEGRATION_TEST", "1") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .current_dir(ord_data_dir) + .arg("--regtest") + .arg("--data-dir") + .arg(ord_data_dir) + .arg("--rpc-url") + .arg(&format!("127.0.0.1:{}", rpc_port)) + .arg("--cookie-file") + .arg(cookiefile.to_str().unwrap()) + .args(args); + + let output = ord.output().unwrap(); + + if output.status.success() { + Ok(String::from(str::from_utf8(&output.stdout).unwrap())) + } else { + Err(String::from(str::from_utf8(&output.stderr).unwrap())) + } +} + +#[test] +#[ignore] +fn inscribe_child() { + let rpc_port = get_free_port(); + + let tmp_dir_1 = TempDir::new().unwrap(); + let bitcoin_data_dir = tmp_dir_1.path().join("bitcoin"); + fs::create_dir(&bitcoin_data_dir).unwrap(); + + let tmp_dir_2 = TempDir::new().unwrap(); + let ord_data_dir = tmp_dir_2.path().join("ord"); + fs::create_dir(&ord_data_dir).unwrap(); + + let _bitcoind = KillOnDrop( + Command::new("bitcoind") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .arg({ + let mut arg = OsString::from("-datadir="); + arg.push(&bitcoin_data_dir); + arg + }) + .arg("-regtest") + .arg("-txindex") + .arg("-listen=0") + .arg("-minrelaytxfee=0") + .arg(format!("-rpcport={rpc_port}")) + .spawn() + .expect("failed to spawn `bitcoind`"), + ); + + let cookiefile = bitcoin_data_dir.as_path().join("regtest/.cookie"); + + for attempt in 0.. { + if Client::new( + &format!("127.0.0.1:{rpc_port}"), + bitcoincore_rpc::Auth::CookieFile(cookiefile.clone()), + ) + .is_ok() + { + break; + } + + if attempt == 500 { + panic!("Bitcoin Core RPC did not respond"); + } + + thread::sleep(Duration::from_millis(50)); + } + + thread::sleep(Duration::from_secs(1)); + + let _ = ord(&cookiefile, &ord_data_dir, rpc_port, &["wallet", "create"]); + + // get funds in wallet + // inscribe parent + // mine block + // inscribe child with parent + + let rpc_client = Client::new( + &format!("127.0.0.1:{rpc_port}/wallet/ord"), + bitcoincore_rpc::Auth::CookieFile(cookiefile.clone()), + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + let address = rpc_client + .get_new_address(None, Some(bitcoincore_rpc::json::AddressType::Bech32m)) + .unwrap(); + + rpc_client.generate_to_address(1, &address).unwrap(); + let random_address = Address::from_str("bcrt1p998q5wmfjdwxkxpjcrnsfytdgu24qazas9gcjdfzl2asq9s38qtsgl8men").unwrap(); + rpc_client.generate_to_address(100, &random_address).unwrap(); + + fs::write(ord_data_dir.as_path().join("parent.txt"), "Pater").unwrap(); + + #[derive(Deserialize, Debug)] + struct Output { + commit: String, + inscription: String, + parent: Option, + reveal: String, + fees: u64, + } + + let output: Output = match ord( + &cookiefile, + &ord_data_dir, + rpc_port, + &["wallet", "inscribe", "parent.txt"], + ) { + Ok(s) => serde_json::from_str(&s) + .unwrap_or_else(|err| panic!("Failed to deserialize JSON: {err}\n{s}")), + Err(e) => panic!("error inscribing parent: {}", e), + }; + + let parent_id = output.inscription; + + rpc_client.generate_to_address(1, &address).unwrap(); + + thread::sleep(Duration::from_secs(1)); + + fs::write(ord_data_dir.as_path().join("child.txt"), "Filius").unwrap(); + let output: Output = match ord( + &cookiefile, + &ord_data_dir, + rpc_port, + &["wallet", "inscribe", "--parent", &parent_id, "child.txt"], + ) { + Ok(s) => serde_json::from_str(&s) + .unwrap_or_else(|err| panic!("Failed to deserialize JSON: {err}\n{s}")), + Err(e) => panic!("error inscribing child with parent: {}", e), + }; + + let child_id = output.inscription; + let ord_port = 8080; + + rpc_client.generate_to_address(1, &address).unwrap(); + + let _ord_server = KillOnDrop( + Command::new(executable_path("ord")) + .env("ORD_INTEGRATION_TEST", "1") + .stdin(Stdio::null()) + // .stdout(Stdio::piped()) + // .stderr(Stdio::piped()) + .current_dir(ord_data_dir.clone()) + .arg("--regtest") + .arg("--data-dir") + .arg(ord_data_dir.as_path()) + .arg("--rpc-url") + .arg(&format!("127.0.0.1:{}", rpc_port)) + .arg("--cookie-file") + .arg(cookiefile.to_str().unwrap()) + .arg("server") + .arg("--http-port") + .arg(&format!("{ord_port}")) + .spawn() + .expect("failed to spawn `ord server`"), + ); + + let client = reqwest::blocking::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap(); + + for i in 0.. { + match client + .get(format!("http://127.0.0.1:{ord_port}/status")) + .send() + { + Ok(_) => break, + Err(err) => { + if i == 400 { + panic!("server failed to start: {err}"); + } + } + } + + thread::sleep(Duration::from_millis(25)); + } + + let response = client + .get(format!( + "http://127.0.0.1:{ord_port}/inscription/{parent_id}" + )) + .send() + .unwrap(); + + assert_regex_match!(response.text().unwrap(), &format!(".*id.*{}.*", parent_id)); + + thread::sleep(Duration::from_secs(10)); + + let response = client + .get(format!( + "http://127.0.0.1:{ord_port}/inscription/{child_id}" + )) + .send() + .unwrap(); + + assert_regex_match!( + response.text().unwrap(), + &format!(".*parent.*{}.*", parent_id) + ); +} diff --git a/tests/lib.rs b/tests/lib.rs index 710e6b2a88..b871a03588 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -43,6 +43,7 @@ struct Inscribe { inscription: String, reveal: Txid, fees: u64, + parent: Option, } fn inscribe(rpc_server: &test_bitcoincore_rpc::Handle) -> Inscribe { diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index 16b486037e..fdd45a8150 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -11,7 +11,8 @@ fn inscribe_creates_inscriptions() { let Inscribe { inscription, .. } = inscribe(&rpc_server); - assert_eq!(rpc_server.descriptors().len(), 3); + // no backup + assert_eq!(rpc_server.descriptors().len(), 2); let request = TestServer::spawn_with_args(&rpc_server, &[]).request(format!("/content/{inscription}")); @@ -335,7 +336,7 @@ fn inscribe_with_dry_run_flag() { } #[test] -fn inscribe_with_dry_run_flag_fees_inscrease() { +fn inscribe_with_dry_run_flag_fees_increase() { let rpc_server = test_bitcoincore_rpc::spawn(); create_wallet(&rpc_server); rpc_server.mine_blocks(1); @@ -394,3 +395,53 @@ fn inscribe_with_no_limit() { .write("degenerate.png", four_megger) .rpc_server(&rpc_server); } + +#[test] +fn inscribe_with_parent_inscription() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(1); + + let parent_id = CommandBuilder::new("wallet inscribe parent.png") + .write("parent.png", [1; 520]) + .rpc_server(&rpc_server) + .output::() + .inscription; + + rpc_server.mine_blocks(1); + + TestServer::spawn_with_args(&rpc_server, &[]) + .assert_response_regex(format!("/inscription/{parent_id}"), ".*"); + + let child_output = CommandBuilder::new(format!("wallet inscribe --parent {parent_id} child.png")) + .write("child.png", [1; 520]) + .rpc_server(&rpc_server) + .output::(); + + assert_eq!(parent_id, child_output.parent.unwrap()); + + rpc_server.mine_blocks(1); + + TestServer::spawn_with_args(&rpc_server, &[]).assert_response_regex( + format!("/inscription/{}", child_output.inscription), + format!(".*parent.*{}.*", parent_id), + ); +} + +#[test] +fn inscribe_with_non_existent_parent_inscription() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(1); + + let parent_id = "3ac40a8f3c0d295386e1e597467a1ee0578df780834be885cd62337c2ed738a5i0"; + + CommandBuilder::new(format!("wallet inscribe --parent {parent_id} child.png")) + .write("child.png", [1; 520]) + .rpc_server(&rpc_server) + .expected_stderr(format!( + "error: specified parent {parent_id} does not exist\n" + )) + .expected_exit_code(1) + .run(); +}