From c654f140e610a54286a2bed461507af795fbb357 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 8 Jan 2024 17:58:53 -0800 Subject: [PATCH 001/188] Add recipe to deploy to all servers in fleet (#2992) --- justfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/justfile b/justfile index 296f35809d..ef6cfdcd70 100644 --- a/justfile +++ b/justfile @@ -37,6 +37,14 @@ deploy-signet branch='master' remote='ordinals/ord': (deploy branch remote 'sign deploy-testnet branch='master' remote='ordinals/ord': (deploy branch remote 'test' 'testnet.ordinals.net') +deploy-all: \ + deploy-regtest \ + deploy-testnet \ + deploy-signet \ + deploy-mainnet-alpha \ + deploy-mainnet-bravo \ + deploy-mainnet-charlie + servers := 'alpha bravo charlie regtest signet testnet' initialize-server-keys: From deb968f3cb0c0ce2caa21416f7a9e92d8fef82f7 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 9 Jan 2024 04:48:15 -0800 Subject: [PATCH 002/188] Suppress empty command output (#2995) --- src/lib.rs | 6 +++++- src/subcommand.rs | 5 +---- src/subcommand/balances.rs | 4 ++-- src/subcommand/decode.rs | 6 +++--- src/subcommand/epochs.rs | 2 +- src/subcommand/find.rs | 4 ++-- src/subcommand/index/export.rs | 2 +- src/subcommand/index/info.rs | 4 ++-- src/subcommand/index/update.rs | 2 +- src/subcommand/list.rs | 2 +- src/subcommand/parse.rs | 4 ++-- src/subcommand/preview.rs | 2 +- src/subcommand/runes.rs | 4 ++-- src/subcommand/server.rs | 2 +- src/subcommand/subsidy.rs | 4 ++-- src/subcommand/supply.rs | 4 ++-- src/subcommand/teleburn.rs | 4 ++-- src/subcommand/traits.rs | 4 ++-- src/subcommand/wallet/balance.rs | 4 ++-- src/subcommand/wallet/cardinals.rs | 2 +- src/subcommand/wallet/create.rs | 4 ++-- src/subcommand/wallet/etch.rs | 4 ++-- src/subcommand/wallet/inscribe/batch.rs | 8 ++++---- src/subcommand/wallet/inscriptions.rs | 2 +- src/subcommand/wallet/outputs.rs | 2 +- src/subcommand/wallet/receive.rs | 2 +- src/subcommand/wallet/restore.rs | 2 +- src/subcommand/wallet/sats.rs | 4 ++-- src/subcommand/wallet/send.rs | 6 +++--- src/subcommand/wallet/transactions.rs | 2 +- tests/index.rs | 11 +++++------ tests/wallet/restore.rs | 6 +++--- 32 files changed, 62 insertions(+), 62 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9102594701..27662d0f9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -248,7 +248,11 @@ pub fn main() { process::exit(1); } - Ok(output) => output.print_json(), + Ok(output) => { + if let Some(output) = output { + output.print_json(); + } + } } gracefully_shutdown_indexer(); diff --git a/src/subcommand.rs b/src/subcommand.rs index ff7b406079..bc158a1b4e 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -77,9 +77,6 @@ impl Subcommand { } } -#[derive(Serialize, Deserialize)] -pub struct Empty {} - pub(crate) trait Output: Send { fn print_json(&self); } @@ -94,4 +91,4 @@ where } } -pub(crate) type SubcommandResult = Result>; +pub(crate) type SubcommandResult = Result>>; diff --git a/src/subcommand/balances.rs b/src/subcommand/balances.rs index cc94155bbf..35424bc879 100644 --- a/src/subcommand/balances.rs +++ b/src/subcommand/balances.rs @@ -15,7 +15,7 @@ pub(crate) fn run(options: Options) -> SubcommandResult { index.update()?; - Ok(Box::new(Output { + Ok(Some(Box::new(Output { runes: index.get_rune_balance_map()?, - })) + }))) } diff --git a/src/subcommand/decode.rs b/src/subcommand/decode.rs index 38d2275524..3d3184ff86 100644 --- a/src/subcommand/decode.rs +++ b/src/subcommand/decode.rs @@ -88,15 +88,15 @@ impl Decode { let inscriptions = ParsedEnvelope::from_transaction(&transaction); if self.compact { - Ok(Box::new(CompactOutput { + Ok(Some(Box::new(CompactOutput { inscriptions: inscriptions .clone() .into_iter() .map(|inscription| inscription.payload.try_into()) .collect::>>()?, - })) + }))) } else { - Ok(Box::new(RawOutput { inscriptions })) + Ok(Some(Box::new(RawOutput { inscriptions }))) } } } diff --git a/src/subcommand/epochs.rs b/src/subcommand/epochs.rs index 39534a65ca..b5971f49f2 100644 --- a/src/subcommand/epochs.rs +++ b/src/subcommand/epochs.rs @@ -11,5 +11,5 @@ pub(crate) fn run() -> SubcommandResult { starting_sats.push(sat); } - Ok(Box::new(Output { starting_sats })) + Ok(Some(Box::new(Output { starting_sats }))) } diff --git a/src/subcommand/find.rs b/src/subcommand/find.rs index 68884679df..1f35bc518e 100644 --- a/src/subcommand/find.rs +++ b/src/subcommand/find.rs @@ -32,11 +32,11 @@ impl Find { match self.end { Some(end) => match index.find_range(self.sat, end)? { - Some(result) => Ok(Box::new(result)), + Some(result) => Ok(Some(Box::new(result))), None => Err(anyhow!("range has not been mined as of index height")), }, None => match index.find(self.sat)? { - Some(satpoint) => Ok(Box::new(Output { satpoint })), + Some(satpoint) => Ok(Some(Box::new(Output { satpoint }))), None => Err(anyhow!("sat has not been mined as of index height")), }, } diff --git a/src/subcommand/index/export.rs b/src/subcommand/index/export.rs index cddebe59bb..4cb45af531 100644 --- a/src/subcommand/index/export.rs +++ b/src/subcommand/index/export.rs @@ -15,6 +15,6 @@ impl Export { index.update()?; index.export(&self.tsv, self.include_addresses)?; - Ok(Box::new(Empty {})) + Ok(None) } } diff --git a/src/subcommand/index/info.rs b/src/subcommand/index/info.rs index 8b9a2deb07..3773e43eb3 100644 --- a/src/subcommand/index/info.rs +++ b/src/subcommand/index/info.rs @@ -34,9 +34,9 @@ impl Info { elapsed: (end.starting_timestamp - start.starting_timestamp) as f64 / 1000.0 / 60.0, }); } - Ok(Box::new(output)) + Ok(Some(Box::new(output))) } else { - Ok(Box::new(info)) + Ok(Some(Box::new(info))) } } } diff --git a/src/subcommand/index/update.rs b/src/subcommand/index/update.rs index cd3d7d45e8..62a82ff838 100644 --- a/src/subcommand/index/update.rs +++ b/src/subcommand/index/update.rs @@ -5,5 +5,5 @@ pub(crate) fn run(options: Options) -> SubcommandResult { index.update()?; - Ok(Box::new(Empty {})) + Ok(None) } diff --git a/src/subcommand/list.rs b/src/subcommand/list.rs index c0477407c0..8de7f8e0ea 100644 --- a/src/subcommand/list.rs +++ b/src/subcommand/list.rs @@ -51,7 +51,7 @@ impl List { }); } - Ok(Box::new(outputs)) + Ok(Some(Box::new(outputs))) } Some(crate::index::List::Spent) => Err(anyhow!("output spent.")), None => Err(anyhow!("output not found")), diff --git a/src/subcommand/parse.rs b/src/subcommand/parse.rs index 2430abd185..b3c1067a05 100644 --- a/src/subcommand/parse.rs +++ b/src/subcommand/parse.rs @@ -13,8 +13,8 @@ pub struct Output { impl Parse { pub(crate) fn run(self) -> SubcommandResult { - Ok(Box::new(Output { + Ok(Some(Box::new(Output { object: self.object, - })) + }))) } } diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index 845c0c90d6..effada817d 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -200,6 +200,6 @@ impl Preview { .run()?; } - Ok(Box::new(Empty {})) + Ok(None) } } diff --git a/src/subcommand/runes.rs b/src/subcommand/runes.rs index ed28f68bd8..854d04a37e 100644 --- a/src/subcommand/runes.rs +++ b/src/subcommand/runes.rs @@ -35,7 +35,7 @@ pub(crate) fn run(options: Options) -> SubcommandResult { index.update()?; - Ok(Box::new(Output { + Ok(Some(Box::new(Output { runes: index .runes()? .into_iter() @@ -82,5 +82,5 @@ pub(crate) fn run(options: Options) -> SubcommandResult { }, ) .collect::>(), - })) + }))) } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index a9806f4181..cdcacab008 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -334,7 +334,7 @@ impl Server { (None, None) => unreachable!(), } - Ok(Box::new(Empty {}) as Box) + Ok(None) }) } diff --git a/src/subcommand/subsidy.rs b/src/subcommand/subsidy.rs index 9f0db8a981..52d2c7d6d1 100644 --- a/src/subcommand/subsidy.rs +++ b/src/subcommand/subsidy.rs @@ -23,10 +23,10 @@ impl Subsidy { bail!("block {} has no subsidy", self.height); } - Ok(Box::new(Output { + Ok(Some(Box::new(Output { first: first.0, subsidy, name: first.name(), - })) + }))) } } diff --git a/src/subcommand/supply.rs b/src/subcommand/supply.rs index 66622ef361..f6ca1d8876 100644 --- a/src/subcommand/supply.rs +++ b/src/subcommand/supply.rs @@ -18,10 +18,10 @@ pub(crate) fn run() -> SubcommandResult { last += 1; } - Ok(Box::new(Output { + Ok(Some(Box::new(Output { supply: Sat::SUPPLY, first: 0, last: Sat::SUPPLY - 1, last_mined_in_block: last, - })) + }))) } diff --git a/src/subcommand/teleburn.rs b/src/subcommand/teleburn.rs index e13d5d57e3..d0181021b9 100644 --- a/src/subcommand/teleburn.rs +++ b/src/subcommand/teleburn.rs @@ -13,8 +13,8 @@ pub struct Output { impl Teleburn { pub(crate) fn run(self) -> SubcommandResult { - Ok(Box::new(Output { + Ok(Some(Box::new(Output { ethereum: self.destination.into(), - })) + }))) } } diff --git a/src/subcommand/traits.rs b/src/subcommand/traits.rs index ce2bce499a..05f177f526 100644 --- a/src/subcommand/traits.rs +++ b/src/subcommand/traits.rs @@ -22,7 +22,7 @@ pub struct Output { impl Traits { pub(crate) fn run(self) -> SubcommandResult { - Ok(Box::new(Output { + Ok(Some(Box::new(Output { number: self.sat.n(), decimal: self.sat.decimal().to_string(), degree: self.sat.degree().to_string(), @@ -33,6 +33,6 @@ impl Traits { period: self.sat.period(), offset: self.sat.third(), rarity: self.sat.rarity(), - })) + }))) } } diff --git a/src/subcommand/wallet/balance.rs b/src/subcommand/wallet/balance.rs index 8a95985e44..ae88dc2aba 100644 --- a/src/subcommand/wallet/balance.rs +++ b/src/subcommand/wallet/balance.rs @@ -45,13 +45,13 @@ pub(crate) fn run(wallet: String, options: Options) -> SubcommandResult { } } - Ok(Box::new(Output { + Ok(Some(Box::new(Output { cardinal, ordinal, runes: index.has_rune_index().then_some(runes), runic: index.has_rune_index().then_some(runic), total: cardinal + ordinal + runic, - })) + }))) } #[cfg(test)] diff --git a/src/subcommand/wallet/cardinals.rs b/src/subcommand/wallet/cardinals.rs index ab6e932521..f809571e34 100644 --- a/src/subcommand/wallet/cardinals.rs +++ b/src/subcommand/wallet/cardinals.rs @@ -35,5 +35,5 @@ pub(crate) fn run(wallet: String, options: Options) -> SubcommandResult { }) .collect::>(); - Ok(Box::new(cardinal_utxos)) + Ok(Some(Box::new(cardinal_utxos))) } diff --git a/src/subcommand/wallet/create.rs b/src/subcommand/wallet/create.rs index 6b8fb9b0a0..69121d2746 100644 --- a/src/subcommand/wallet/create.rs +++ b/src/subcommand/wallet/create.rs @@ -25,9 +25,9 @@ impl Create { wallet::initialize(wallet, &options, mnemonic.to_seed(self.passphrase.clone()))?; - Ok(Box::new(Output { + Ok(Some(Box::new(Output { mnemonic, passphrase: Some(self.passphrase), - })) + }))) } } diff --git a/src/subcommand/wallet/etch.rs b/src/subcommand/wallet/etch.rs index 1c00a36370..2f863f7182 100644 --- a/src/subcommand/wallet/etch.rs +++ b/src/subcommand/wallet/etch.rs @@ -124,9 +124,9 @@ impl Etch { let transaction = client.send_raw_transaction(&signed_transaction)?; - Ok(Box::new(Output { + Ok(Some(Box::new(Output { rune: self.rune, transaction, - })) + }))) } } diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 1024097825..31671effa0 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -62,12 +62,12 @@ impl Batch { )?; if self.dry_run { - return Ok(Box::new(self.output( + return Ok(Some(Box::new(self.output( commit_tx.txid(), reveal_tx.txid(), total_fees, self.inscriptions.clone(), - ))); + )))); } let signed_commit_tx = client @@ -114,12 +114,12 @@ impl Batch { } }; - Ok(Box::new(self.output( + Ok(Some(Box::new(self.output( commit, reveal, total_fees, self.inscriptions.clone(), - ))) + )))) } fn output( diff --git a/src/subcommand/wallet/inscriptions.rs b/src/subcommand/wallet/inscriptions.rs index 5cf2c682f8..0d58297fff 100644 --- a/src/subcommand/wallet/inscriptions.rs +++ b/src/subcommand/wallet/inscriptions.rs @@ -38,5 +38,5 @@ pub(crate) fn run(wallet: String, options: Options) -> SubcommandResult { } } - Ok(Box::new(output)) + Ok(Some(Box::new(output))) } diff --git a/src/subcommand/wallet/outputs.rs b/src/subcommand/wallet/outputs.rs index 9c4655b61a..b37faadbea 100644 --- a/src/subcommand/wallet/outputs.rs +++ b/src/subcommand/wallet/outputs.rs @@ -21,5 +21,5 @@ pub(crate) fn run(wallet: String, options: Options) -> SubcommandResult { }); } - Ok(Box::new(outputs)) + Ok(Some(Box::new(outputs))) } diff --git a/src/subcommand/wallet/receive.rs b/src/subcommand/wallet/receive.rs index 2ff8afc481..fe7d40ec2a 100644 --- a/src/subcommand/wallet/receive.rs +++ b/src/subcommand/wallet/receive.rs @@ -9,5 +9,5 @@ pub(crate) fn run(wallet: String, options: Options) -> SubcommandResult { let address = bitcoin_rpc_client_for_wallet_command(wallet, &options)? .get_new_address(None, Some(bitcoincore_rpc::json::AddressType::Bech32m))?; - Ok(Box::new(Output { address })) + Ok(Some(Box::new(Output { address }))) } diff --git a/src/subcommand/wallet/restore.rs b/src/subcommand/wallet/restore.rs index d4e48b6b78..54c2d64d4e 100644 --- a/src/subcommand/wallet/restore.rs +++ b/src/subcommand/wallet/restore.rs @@ -20,6 +20,6 @@ impl Restore { self.mnemonic.to_seed(self.passphrase), )?; - Ok(Box::new(Empty {})) + Ok(None) } } diff --git a/src/subcommand/wallet/sats.rs b/src/subcommand/wallet/sats.rs index 926610f9b8..65a0c2491d 100644 --- a/src/subcommand/wallet/sats.rs +++ b/src/subcommand/wallet/sats.rs @@ -49,7 +49,7 @@ impl Sats { output: outpoint, }); } - Ok(Box::new(output)) + Ok(Some(Box::new(output))) } else { let mut output = Vec::new(); for (outpoint, sat, offset, rarity) in rare_sats(utxos) { @@ -60,7 +60,7 @@ impl Sats { rarity, }); } - Ok(Box::new(output)) + Ok(Some(Box::new(output))) } } } diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index bf1d98b0c4..39eed8712f 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -46,7 +46,7 @@ impl Send { Outgoing::Amount(amount) => { Self::lock_non_cardinal_outputs(&client, &inscriptions, &runic_outputs, unspent_outputs)?; let transaction = Self::send_amount(&client, amount, address, self.fee_rate)?; - return Ok(Box::new(Output { transaction })); + return Ok(Some(Box::new(Output { transaction }))); } Outgoing::InscriptionId(id) => index .get_inscription_satpoint_by_id(id)? @@ -64,7 +64,7 @@ impl Send { runic_outputs, unspent_outputs, )?; - return Ok(Box::new(Output { transaction })); + return Ok(Some(Box::new(Output { transaction }))); } Outgoing::SatPoint(satpoint) => { for inscription_satpoint in inscriptions.keys() { @@ -112,7 +112,7 @@ impl Send { let txid = client.send_raw_transaction(&signed_tx)?; - Ok(Box::new(Output { transaction: txid })) + Ok(Some(Box::new(Output { transaction: txid }))) } fn lock_non_cardinal_outputs( diff --git a/src/subcommand/wallet/transactions.rs b/src/subcommand/wallet/transactions.rs index 8c11410127..d212f2200e 100644 --- a/src/subcommand/wallet/transactions.rs +++ b/src/subcommand/wallet/transactions.rs @@ -29,6 +29,6 @@ impl Transactions { }); } - Ok(Box::new(output)) + Ok(Some(Box::new(output))) } } diff --git a/tests/index.rs b/tests/index.rs index 72e3a20ecf..663acb9f8b 100644 --- a/tests/index.rs +++ b/tests/index.rs @@ -1,4 +1,4 @@ -use {super::*, crate::command_builder::ToArgs, ord::subcommand::Empty}; +use {super::*, crate::command_builder::ToArgs}; #[test] fn run_is_an_alias_for_update() { @@ -11,7 +11,7 @@ fn run_is_an_alias_for_update() { CommandBuilder::new(format!("--index {} index run", index_path.display())) .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .run_and_extract_stdout(); assert!(index_path.is_file()) } @@ -27,7 +27,7 @@ fn custom_index_path() { CommandBuilder::new(format!("--index {} index update", index_path.display())) .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .run_and_extract_stdout(); assert!(index_path.is_file()) } @@ -43,13 +43,13 @@ fn re_opening_database_does_not_trigger_schema_check() { CommandBuilder::new(format!("--index {} index update", index_path.display())) .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .run_and_extract_stdout(); assert!(index_path.is_file()); CommandBuilder::new(format!("--index {} index update", index_path.display())) .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .run_and_extract_stdout(); } #[test] @@ -96,7 +96,6 @@ fn export_inscription_number_to_id_tsv() { let tsv = CommandBuilder::new("index export --tsv foo.tsv") .rpc_server(&rpc_server) .temp_dir(Arc::new(temp_dir)) - .stdout_regex(r"\{\}\n") .run_and_extract_file("foo.tsv"); let entries: std::collections::BTreeMap = tsv diff --git a/tests/wallet/restore.rs b/tests/wallet/restore.rs index 5106c5eb33..f13e601a70 100644 --- a/tests/wallet/restore.rs +++ b/tests/wallet/restore.rs @@ -1,4 +1,4 @@ -use {super::*, ord::subcommand::wallet::create, ord::subcommand::Empty}; +use {super::*, ord::subcommand::wallet::create}; #[test] fn restore_generates_same_descriptors() { @@ -16,7 +16,7 @@ fn restore_generates_same_descriptors() { CommandBuilder::new(["wallet", "restore", &mnemonic.to_string()]) .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .run_and_extract_stdout(); assert_eq!(rpc_server.descriptors(), descriptors); } @@ -45,7 +45,7 @@ fn restore_generates_same_descriptors_with_passphrase() { &mnemonic.to_string(), ]) .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .run_and_extract_stdout(); assert_eq!(rpc_server.descriptors(), descriptors); } From adb6f25b434125a84c302d7bf2f60e1834cad148 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 9 Jan 2024 08:27:11 -0800 Subject: [PATCH 003/188] Optimize get_inscription_ids_by_sat_paginated (#2996) --- src/index.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.rs b/src/index.rs index 2ecde7af18..d97a90786f 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1257,6 +1257,8 @@ impl Index { let mut ids = rtx .open_multimap_table(SAT_TO_SEQUENCE_NUMBER)? .get(&sat.n())? + .skip(page_index.saturating_mul(page_size).try_into().unwrap()) + .take(page_size.saturating_add(1).try_into().unwrap()) .map(|result| { result .and_then(|sequence_number| { @@ -1267,8 +1269,6 @@ impl Index { }) .map_err(|err| err.into()) }) - .skip(page_index.saturating_mul(page_size).try_into().unwrap()) - .take(page_size.saturating_add(1).try_into().unwrap()) .collect::>>()?; let more = ids.len() > page_size.try_into().unwrap(); From bf7ad35177ec0df4cd8753dc6ecbc72e285e4135 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 9 Jan 2024 08:33:44 -0800 Subject: [PATCH 004/188] Add crate to audit content security policy (#2993) --- Cargo.lock | 8 ++ .../audit-content-security-policy/Cargo.toml | 9 ++ .../audit-content-security-policy/src/main.rs | 93 +++++++++++++++++++ justfile | 3 + 4 files changed, 113 insertions(+) create mode 100644 crates/audit-content-security-policy/Cargo.toml create mode 100644 crates/audit-content-security-policy/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 4882ab9a53..8ce8745b51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -344,6 +344,14 @@ dependencies = [ "reqwest", ] +[[package]] +name = "audit-content-security-policy" +version = "0.0.0" +dependencies = [ + "colored", + "reqwest", +] + [[package]] name = "autocfg" version = "1.1.0" diff --git a/crates/audit-content-security-policy/Cargo.toml b/crates/audit-content-security-policy/Cargo.toml new file mode 100644 index 0000000000..e57d9da2e7 --- /dev/null +++ b/crates/audit-content-security-policy/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "audit-content-security-policy" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +colored = "2.0.4" +reqwest = { version = "0.11.22", features = ["blocking"] } diff --git a/crates/audit-content-security-policy/src/main.rs b/crates/audit-content-security-policy/src/main.rs new file mode 100644 index 0000000000..1b4776d72a --- /dev/null +++ b/crates/audit-content-security-policy/src/main.rs @@ -0,0 +1,93 @@ +use {colored::Colorize, reqwest::blocking::get, std::process}; + +const SERVERS: &[(&str, &str, &str)] = &[ + ( + "regtest.ordinals.net", + "/content/41bf99a297ca79d181160a91fc0efc8a71170ee24b87783c9c11b0fcbe23615fi0", + "https://regtest.ordinals.com/content/", + ), + ( + "regtest.ordinals.com", + "/content/41bf99a297ca79d181160a91fc0efc8a71170ee24b87783c9c11b0fcbe23615fi0", + "https://regtest.ordinals.com/content/", + ), + ( + "signet.ordinals.net", + "/content/7e1bc3b56b872aaf4d1aaf1565fac72182313c9142b207f9398afe263e234135i0", + "https://signet.ordinals.com/content/", + ), + ( + "signet.ordinals.com", + "/content/7e1bc3b56b872aaf4d1aaf1565fac72182313c9142b207f9398afe263e234135i0", + "https://signet.ordinals.com/content/", + ), + ( + "testnet.ordinals.net", + "/content/0a1b4e4acf89686e4d012561014041bffd57a62254486f24cb5b0a216c04f102i0", + "https://testnet.ordinals.com/content/", + ), + ( + "testnet.ordinals.com", + "/content/0a1b4e4acf89686e4d012561014041bffd57a62254486f24cb5b0a216c04f102i0", + "https://testnet.ordinals.com/content/", + ), + ( + "alpha.ordinals.net", + "/content/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0", + "https://ordinals.com/content/", + ), + ( + "bravo.ordinals.net", + "/content/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0", + "https://ordinals.com/content/", + ), + ( + "charlie.ordinals.net", + "/content/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0", + "https://ordinals.com/content/", + ), + ( + "ordinals.com", + "/content/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0", + "https://ordinals.com/content/", + ), +]; + +fn main() { + let mut failures = 0; + + for (host, path, needle) in SERVERS { + eprint!("GET {host}"); + + let response = get(format!("https://{host}{path}")).unwrap(); + + let mut fail = false; + + if !response.status().is_success() { + eprint!(" {}", response.status().to_string().red()); + fail = true; + } + + let headers = response.headers(); + + let content_security_policy = headers + .get("content-security-policy") + .map(|value| value.to_str().unwrap().to_string()) + .unwrap_or_default(); + + if !content_security_policy.contains(needle) { + fail = true; + } + + if fail { + eprintln!(" {}", "FAIL".red()); + failures += 1; + } else { + eprintln!(" {}", "PASS".green()); + } + } + + if failures > 0 { + process::exit(1); + } +} diff --git a/justfile b/justfile index ef6cfdcd70..982c57f066 100644 --- a/justfile +++ b/justfile @@ -181,6 +181,9 @@ update-mdbook-theme: audit-cache: cargo run --package audit-cache +audit-content-security-policy: + cargo run --package audit-content-security-policy + coverage: cargo llvm-cov From 61593470af6e2389f03d0ad82ccc95233670084d Mon Sep 17 00:00:00 2001 From: oxSaturn Date: Thu, 11 Jan 2024 00:07:36 +0800 Subject: [PATCH 005/188] Remove dead link from README (#3000) --- docs/src/introduction.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/introduction.md b/docs/src/introduction.md index 28686f5270..866da74224 100644 --- a/docs/src/introduction.md +++ b/docs/src/introduction.md @@ -48,4 +48,3 @@ Videos - [Ordinal Theory Explained: Satoshi Serial Numbers and NFTs on Bitcoin](https://www.youtube.com/watch?v=rSS0O2KQpsI) - [Ordinals Workshop with Rodarmor](https://www.youtube.com/watch?v=MC_haVa6N3I) -- [Ordinal Art: Mint Your own NFTs on Bitcoin w/ @rodarmor](https://www.youtube.com/watch?v=j5V33kV3iqo) From 331dbb12d95e7c26716131ba69e192ece561e641 Mon Sep 17 00:00:00 2001 From: raph Date: Wed, 10 Jan 2024 23:35:32 +0100 Subject: [PATCH 006/188] Add `indexed` to output JSON (#2971) --- src/index.rs | 11 +++++++++++ src/subcommand/server.rs | 8 ++++++++ src/templates/output.rs | 3 +++ tests/json_api.rs | 22 ++++++++++++++++++++++ tests/wallet/send.rs | 1 + 5 files changed, 45 insertions(+) diff --git a/src/index.rs b/src/index.rs index d97a90786f..4b66d0fda1 100644 --- a/src/index.rs +++ b/src/index.rs @@ -395,6 +395,17 @@ impl Index { Ok(true) } + pub(crate) fn contains_output(&self, output: &OutPoint) -> Result { + Ok( + self + .database + .begin_read()? + .open_table(OUTPOINT_TO_VALUE)? + .get(&output.store())? + .is_some(), + ) + } + pub(crate) fn has_rune_index(&self) -> bool { self.index_runes } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index cdcacab008..78943a2af1 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -548,6 +548,8 @@ impl Server { task::block_in_place(|| { let list = index.list(outpoint)?; + let indexed; + let output = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { let mut value = 0; @@ -557,11 +559,15 @@ impl Server { } } + indexed = true; + TxOut { value, script_pubkey: ScriptBuf::new(), } } else { + indexed = index.contains_output(&outpoint)?; + index .get_transaction(outpoint.txid)? .ok_or_not_found(|| format!("output {outpoint}"))? @@ -582,6 +588,7 @@ impl Server { server_config.chain, output, inscriptions, + indexed, runes .into_iter() .map(|(spaced_rune, pile)| (spaced_rune.rune, pile.amount)) @@ -2611,6 +2618,7 @@ mod tests { address: None, transaction: txid.to_string(), sat_ranges: None, + indexed: true, inscriptions: Vec::new(), runes: vec![(Rune(RUNE), 340282366920938463463374607431768211455)] .into_iter() diff --git a/src/templates/output.rs b/src/templates/output.rs index 08064e073a..73a566038a 100644 --- a/src/templates/output.rs +++ b/src/templates/output.rs @@ -17,6 +17,7 @@ pub struct OutputJson { pub address: Option, pub transaction: String, pub sat_ranges: Option>, + pub indexed: bool, pub inscriptions: Vec, pub runes: BTreeMap, } @@ -28,6 +29,7 @@ impl OutputJson { chain: Chain, output: TxOut, inscriptions: Vec, + indexed: bool, runes: BTreeMap, ) -> Self { Self { @@ -43,6 +45,7 @@ impl OutputJson { Some(List::Unspent(ranges)) => Some(ranges), _ => None, }, + indexed, inscriptions, } } diff --git a/tests/json_api.rs b/tests/json_api.rs index fc0d471a19..a950bc5b49 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -296,8 +296,29 @@ fn get_output() { ], ..Default::default() }); + rpc_server.mine_blocks(1); + let server = TestServer::spawn_with_server_args( + &rpc_server, + &["--index-sats"], + &["--no-sync", "--enable-json-api"], + ); + + let response = reqwest::blocking::Client::new() + .get(server.url().join(&format!("/output/{}:0", txid)).unwrap()) + .header(reqwest::header::ACCEPT, "application/json") + .send() + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + assert!( + !serde_json::from_str::(&response.text().unwrap()) + .unwrap() + .indexed + ); + let server = TestServer::spawn_with_server_args(&rpc_server, &["--index-sats"], &["--enable-json-api"]); @@ -323,6 +344,7 @@ fn get_output() { InscriptionId { txid, index: 1 }, InscriptionId { txid, index: 2 }, ], + indexed: true, runes: BTreeMap::new(), } ); diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index 61c7f8d3fc..0ffe1570ab 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -260,6 +260,7 @@ fn splitting_merged_inscriptions_is_possible() { index: 2 }, ], + indexed: true, runes: BTreeMap::new(), } ); From 0a2203048837990acb57f966350f8be2ebd22e1a Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 12 Jan 2024 10:56:48 -0800 Subject: [PATCH 007/188] Cache less aggressively (#3002) --- src/subcommand/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 78943a2af1..7da13acbff 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1140,7 +1140,7 @@ impl Server { headers.insert( header::CACHE_CONTROL, - HeaderValue::from_static("public, max-age=31536000, immutable"), + HeaderValue::from_static("public, max-age=1209600, immutable"), ); headers.insert( @@ -3921,7 +3921,7 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); assert_eq!( response.headers().get(header::CACHE_CONTROL).unwrap(), - "public, max-age=31536000, immutable" + "public, max-age=1209600, immutable" ); } From a4e2c0f977f131f5a6909c28a1fb62eac8ab99bb Mon Sep 17 00:00:00 2001 From: raph Date: Fri, 12 Jan 2024 20:12:42 +0100 Subject: [PATCH 008/188] Add minimal Dockerfile (#2786) --- .dockerignore | 1 + Dockerfile | 14 ++++++++++++++ README.md | 8 ++++++++ 3 files changed, 23 insertions(+) create mode 120000 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 120000 index 0000000000..3e4e48b0b5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.gitignore \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..6a2a9b7347 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM rust:1.75.0-bookworm as builder + +WORKDIR /usr/src/ord + +COPY . . + +RUN cargo build --bin ord --release + +FROM debian:bookworm-slim + +COPY --from=builder /usr/src/ord/target/release/ord /usr/local/bin + +ENV RUST_BACKTRACE=1 +ENV RUST_LOG=info diff --git a/README.md b/README.md index e0a1d96a9d..31f53f4125 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,14 @@ Once built, the `ord` binary can be found at `./target/release/ord`. `ord` requires `rustc` version 1.67.0 or later. Run `rustc --version` to ensure you have this version. Run `rustup update` to get the latest stable release. +### Docker + +A Docker image can be built with: + +``` +docker build -t ordinals/ord . +``` + ### Homebrew `ord` is available in [Homebrew](https://brew.sh/): From 750f23278b3204fe35c8462e309b2ccb022eea4c Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 12 Jan 2024 11:57:43 -0800 Subject: [PATCH 009/188] Don't use browser sniffing when serving favicon (#3003) --- README.md | 83 ++++++++++++++++++++------------------- docs/theme/favicon.png | 1 + docs/theme/favicon.svg | 1 + justfile | 4 +- src/subcommand/server.rs | 35 ++++------------- src/templates.rs | 4 +- src/test.rs | 6 +-- static/favicon.png | Bin 18588 -> 13375 bytes templates/page.html | 4 +- 9 files changed, 62 insertions(+), 76 deletions(-) create mode 120000 docs/theme/favicon.png create mode 120000 docs/theme/favicon.svg diff --git a/README.md b/README.md index 31f53f4125..ff2d9d7cf5 100644 --- a/README.md +++ b/README.md @@ -261,56 +261,57 @@ Release x.y.z Translations ------------ -To translate [the docs](https://docs.ordinals.com) we use this +To translate [the docs](https://docs.ordinals.com) we use [mdBook i18n helper](https://github.com/google/mdbook-i18n-helpers). -So read through their [usage guide](https://github.com/google/mdbook-i18n-helpers/blob/main/i18n-helpers/USAGE.md) -to see the structure that translations should follow. -There are some other things to watch out for but feel free to just start a -translation and open a PR. Have a look at [this commit](https://github.com/ordinals/ord/commit/329f31bf6dac207dad001507dd6f18c87fdef355) -for an idea of what to do. A maintainer will also help you integrate it into our -build system. +See +[mdbook-i18n-helpers usage guide](https://github.com/google/mdbook-i18n-helpers/blob/main/i18n-helpers/USAGE.md) +for help. -To align your translated version of the Handbook with reference to commit -[#2427](https://github.com/ordinals/ord/pull/2426), here are some guiding -commands to assist you. It is assumed that your local environment is already -well-configured with [Python](https://www.python.org/), -[Mdbook](https://github.com/rust-lang/mdBook), -[mdBook i18n helper](https://github.com/google/mdbook-i18n-helpers) and that you've clone -this repo. +Adding a new translations is somewhat involved, so feel free to start +translation and open a pull request, even if your translation is incomplete. +Take a look at +[this commit](https://github.com/ordinals/ord/commit/329f31bf6dac207dad001507dd6f18c87fdef355) +for an example of adding a new translation. A maintainer will help you integrate it +into our build system. -1. Run the following command to generate a new `pot` file, which is named as -`messages.pot`: +To start a new translation: -``` -MDBOOK_OUTPUT='{"xgettext": {"pot-file": "messages.pot"}}' -mdbook build -d po -``` +1. Install `mdbook`, `mdbook-i18n-helpers`, and `mdbook-linkcheck`: -2. Run `msgmerge` where `xx.po` is your localized language version following -the naming standard of [ISO639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). -This process will update the `po` file with the most recent original version: + ``` + cargo install mdbook mdbook-i18n-helpers mdbook-linkcheck + ``` -``` -msgmerge --update po/xx.po po/messages.pot -``` +2. Generate a new `pot` file named `messages.pot`: -3. Look for `#, fuzzy`. The `mdBook-i18n-helper` tool utilizes the `"fuzzy"` tag -to highlight sections that have been recently edited. You can proceed to perform -the translation tasks by editing the `"fuzzy"`part. + ``` + MDBOOK_OUTPUT='{"xgettext": {"pot-file": "messages.pot"}}' + mdbook build -d po + ``` -4. Execute the `mdbook` command. A demonstration in Chinese (`zh`) is given below: +3. Run `msgmerge` on `XX.po` where `XX` is the two-letter + [ISO-639](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) code for + the language you are translating into. This will update the `po` file with + the text of the most recent English version: -``` -mdbook build docs -d build -MDBOOK_BOOK__LANGUAGE=zh mdbook build docs -d build/zh -mv docs/build/zh/html docs/build/html/zh -python3 -m http.server --directory docs/build/html --bind 127.0.0.1 8080 -``` + ``` + msgmerge --update po/XX.po po/messages.pot + ``` + +4. Untranslated sections are marked with `#, fuzzy` in `XX.po`. Edit the + `msgstr` string with the translated text. + +5. Execute the `mdbook` command to rebuild the docs. For Chinese, whose + two-letter ISO-639 code is `zh`: + + ``` + mdbook build docs -d build + MDBOOK_BOOK__LANGUAGE=zh mdbook build docs -d build/zh + mv docs/build/zh/html docs/build/html/zh + python3 -m http.server --directory docs/build/html --bind 127.0.0.1 8080 + ``` -5. Upon verifying everything and ensuring all is in order, you can commit the -modifications and progress to open a Pull Request (PR) on Github. -(**Note**: Please ensure **ONLY** the **'xx.po'** file is pushed, other files -such as '.pot' or files ending in '~' are **unnecessary** and should **NOT** be -included in the Pull Request.) +6. If everything looks good, commit `XX.po` and open a pull request on GitHub. + Other changed files should be omitted from the pull request. diff --git a/docs/theme/favicon.png b/docs/theme/favicon.png new file mode 120000 index 0000000000..5ca3e9ed5f --- /dev/null +++ b/docs/theme/favicon.png @@ -0,0 +1 @@ +../../static/favicon.png \ No newline at end of file diff --git a/docs/theme/favicon.svg b/docs/theme/favicon.svg new file mode 120000 index 0000000000..13d947797b --- /dev/null +++ b/docs/theme/favicon.svg @@ -0,0 +1 @@ +../../static/favicon.svg \ No newline at end of file diff --git a/justfile b/justfile index 982c57f066..f14591983e 100644 --- a/justfile +++ b/justfile @@ -154,9 +154,11 @@ flamegraph dir=`git branch --show-current`: ./bin/flamegraph $1 serve-docs: build-docs - open http://127.0.0.1:8080 python3 -m http.server --directory docs/build/html --bind 127.0.0.1 8080 +open-docs: + open http://127.0.0.1:8080 + build-docs: #!/usr/bin/env bash mdbook build docs -d build diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 7da13acbff..b34b1c4d72 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -21,11 +21,10 @@ use { axum::{ body, extract::{Extension, Json, Path, Query}, - headers::UserAgent, http::{header, HeaderMap, HeaderValue, StatusCode, Uri}, response::{IntoResponse, Redirect, Response}, routing::get, - Router, TypedHeader, + Router, }, axum_server::Handle, brotli::Decompressor, @@ -880,32 +879,12 @@ impl Server { }) } - async fn favicon(user_agent: Option>) -> ServerResult { - if user_agent - .map(|user_agent| { - user_agent.as_str().contains("Safari/") - && !user_agent.as_str().contains("Chrome/") - && !user_agent.as_str().contains("Chromium/") - }) - .unwrap_or_default() - { - Ok( - Self::static_asset(Path("/favicon.png".to_string())) - .await - .into_response(), - ) - } else { - Ok( - ( - [( - header::CONTENT_SECURITY_POLICY, - HeaderValue::from_static("default-src 'unsafe-inline'"), - )], - Self::static_asset(Path("/favicon.svg".to_string())).await?, - ) - .into_response(), - ) - } + async fn favicon() -> ServerResult { + Ok( + Self::static_asset(Path("/favicon.png".to_string())) + .await + .into_response(), + ) } async fn feed( diff --git a/src/templates.rs b/src/templates.rs index 8906adca8a..87fc226a78 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -134,7 +134,9 @@ mod tests { Foo - + + + diff --git a/src/test.rs b/src/test.rs index 49f8c4fed0..d358b81bda 100644 --- a/src/test.rs +++ b/src/test.rs @@ -16,10 +16,8 @@ macro_rules! assert_regex_match { let string = $value.to_string(); if !regex.is_match(string.as_ref()) { - panic!( - "Regex:\n\n{}\n\n…did not match string:\n\n{}", - regex, string - ); + eprintln!("Regex did not match:"); + pretty_assert_eq!(regex.as_str(), string); } }; } diff --git a/static/favicon.png b/static/favicon.png index fed6cd375908bf0534740cd6abd7482df69de578..bc882e6ebebf2c39678cb2a014e2a7477562fb94 100644 GIT binary patch literal 13375 zcmaia1yo$ivi2Ye1a}Dz!F6zVch|ukf(<0NJ40}{5Zv9J1h<3)hXe@{AOs6;|K`ZO z=iPVT`uAEhyJxzpy1MtS?y9dUT3uBR9pyO+2n0e`keAj3f#85990>U-aDzb0Y#x8P zYsyK2YNkkcfgeNG`U*BGDj+7HjSPAUhXX=*Bza_mdk%W?M;ipvg(LY_+XRl`FB*7& z1`GneLHNMOCej{+_!qq&aDQyFJbwLsSGV`Fe(7ZH;R2;phq_R5vGK6+0R4ywh=0hy z1KJJwaQ_}ZAO3H7aG-pIf42cT5GMyGw;%^t5DcaS^9u6t3-a)QPRcP9fv#t+@&@i8 z5F5?o2hOWl>@^64&}6Tz@1d`vENJQC#Aa^gVqwkZ<>dM(2_)<#2sE9nJ*Rwr>6Xa#KTd9T3fI;o&OC&hF{y$>zz$<^r{42MY)Yuyb&-b8@l*6s+#v&K~Aotj_Kn2h07}s5o+qkf&O*Tyzy#SJd)UEj9PIy%&DzWU z|6qIU`9Il!HTu`I{>NZm=B|G-{jnH->L~oEfuMn=wY$s9SC4ZM<>L6`Vh2Cog#Xa+ zfAs%*et+`n|9^P@+5exs|7O*3w)YU_{A;-X`1?P()S&j(z%%(nTku~j|NQSC^p6WA z_{ZyPAZ_nt?d<+ILrxxEVfO#p^Itjj|4+_;^!$SpSbsqk;BB-r_b`_>_playGyzr) zFe@0W4dxRB3kdRXvU2bXKCbTnqWup!Qc!C^!vB);mm&RA%0DRon+zAIwu{S4(f?-I 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 literal 18588 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 eG+%$O5E|7#@SjbC?X?U024G9FBUV~c82 {{ self.content.title() }} - + + + From 41d9474229288bc68f4166e14e2b6b817c36a057 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 15 Jan 2024 12:16:50 -0800 Subject: [PATCH 010/188] Add option to retain sat index for spent outputs (#2999) --- src/chain.rs | 7 + src/index.rs | 368 ++++++++++++++++++++++------- src/index/testing.rs | 8 +- src/index/updater.rs | 19 +- src/lib.rs | 1 - src/options.rs | 2 + src/subcommand/list.rs | 152 +++++------- src/subcommand/server.rs | 25 +- src/subcommand/wallet.rs | 18 +- src/templates/output.rs | 107 ++++++--- templates/output.html | 14 +- test-bitcoincore-rpc/src/api.rs | 8 + test-bitcoincore-rpc/src/lib.rs | 10 +- test-bitcoincore-rpc/src/server.rs | 48 +++- tests/json_api.rs | 17 +- tests/list.rs | 29 ++- tests/wallet/send.rs | 17 +- 17 files changed, 568 insertions(+), 282 deletions(-) diff --git a/src/chain.rs b/src/chain.rs index 1186ac63ad..e77e6cab0c 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -70,6 +70,13 @@ impl Chain { bitcoin::blockdata::constants::genesis_block(self.network()) } + pub(crate) fn genesis_coinbase_outpoint(self) -> OutPoint { + OutPoint { + txid: self.genesis_block().coinbase().unwrap().txid(), + vout: 0, + } + } + pub(crate) fn address_from_script( self, script: &Script, diff --git a/src/index.rs b/src/index.rs index 4b66d0fda1..d4e2e7f300 100644 --- a/src/index.rs +++ b/src/index.rs @@ -78,12 +78,6 @@ define_table! { TRANSACTION_ID_TO_RUNE, &TxidValue, u128 } define_table! { TRANSACTION_ID_TO_TRANSACTION, &TxidValue, &[u8] } define_table! { WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP, u32, u128 } -#[derive(Debug, PartialEq)] -pub enum List { - Spent, - Unspent(Vec<(u64, u64)>), -} - #[derive(Copy, Clone)] pub(crate) enum Statistic { Schema = 0, @@ -99,6 +93,7 @@ pub(crate) enum Statistic { SatRanges = 10, UnboundInscriptions = 11, IndexTransactions = 12, + IndexSpentSats = 13, } impl Statistic { @@ -197,6 +192,7 @@ pub struct Index { height_limit: Option, index_runes: bool, index_sats: bool, + index_spent_sats: bool, index_transactions: bool, options: Options, path: PathBuf, @@ -237,10 +233,6 @@ impl Index { redb::Durability::Immediate }; - let index_runes; - let index_sats; - let index_transactions; - let index_path = path.clone(); let once = Once::new(); let progress_bar = Mutex::new(None); @@ -269,10 +261,8 @@ impl Index { { Ok(database) => { { - let tx = database.begin_read()?; - let statistics = tx.open_table(STATISTIC_TO_COUNT)?; - - let schema_version = statistics + let schema_version = database.begin_read()? + .open_table(STATISTIC_TO_COUNT)? .get(&Statistic::Schema.key())? .map(|x| x.value()) .unwrap_or(0); @@ -291,11 +281,6 @@ impl Index { cmp::Ordering::Equal => { } } - - - index_runes = Self::is_statistic_set(&statistics, Statistic::IndexRunes)?; - index_sats = Self::is_statistic_set(&statistics, Statistic::IndexSats)?; - index_transactions = Self::is_statistic_set(&statistics, Statistic::IndexTransactions)?; } database @@ -338,14 +323,35 @@ impl Index { outpoint_to_sat_ranges.insert(&OutPoint::null().store(), [].as_slice())?; } - index_runes = options.index_runes(); - index_sats = options.index_sats; - index_transactions = options.index_transactions; - - Self::set_statistic(&mut statistics, Statistic::IndexRunes, u64::from(index_runes))?; - Self::set_statistic(&mut statistics, Statistic::IndexSats, u64::from(index_sats))?; - Self::set_statistic(&mut statistics, Statistic::IndexTransactions, u64::from(index_transactions))?; - Self::set_statistic(&mut statistics, Statistic::Schema, SCHEMA_VERSION)?; + Self::set_statistic( + &mut statistics, + Statistic::IndexRunes, + u64::from(options.index_runes()), + )?; + + Self::set_statistic( + &mut statistics, + Statistic::IndexSats, + u64::from(options.index_sats || options.index_spent_sats), + )?; + + Self::set_statistic( + &mut statistics, + Statistic::IndexSpentSats, + u64::from(options.index_spent_sats), + )?; + + Self::set_statistic( + &mut statistics, + Statistic::IndexTransactions, + u64::from(options.index_transactions), + )?; + + Self::set_statistic( + &mut statistics, + Statistic::Schema, + SCHEMA_VERSION, + )?; } tx.commit()?; @@ -355,6 +361,20 @@ impl Index { Err(error) => bail!("failed to open index: {error}"), }; + let index_runes; + let index_sats; + let index_spent_sats; + let index_transactions; + + { + let tx = database.begin_read()?; + let statistics = tx.open_table(STATISTIC_TO_COUNT)?; + index_runes = Self::is_statistic_set(&statistics, Statistic::IndexRunes)?; + index_sats = Self::is_statistic_set(&statistics, Statistic::IndexSats)?; + index_spent_sats = Self::is_statistic_set(&statistics, Statistic::IndexSpentSats)?; + index_transactions = Self::is_statistic_set(&statistics, Statistic::IndexTransactions)?; + } + let genesis_block_coinbase_transaction = options.chain().genesis_block().coinbase().unwrap().clone(); @@ -368,6 +388,7 @@ impl Index { height_limit: options.height_limit, index_runes, index_sats, + index_spent_sats, index_transactions, options: options.clone(), path, @@ -1481,17 +1502,6 @@ impl Index { ) } - pub(crate) fn is_transaction_in_active_chain(&self, txid: Txid) -> Result { - Ok( - self - .client - .get_raw_transaction_info(&txid, None) - .into_option()? - .and_then(|info| info.in_active_chain) - .unwrap_or(false), - ) - } - pub(crate) fn find(&self, sat: Sat) -> Result> { let sat = sat.0; let rtx = self.begin_read()?; @@ -1573,41 +1583,60 @@ impl Index { Ok(Some(result)) } - fn list_inner(&self, outpoint: OutPointValue) -> Result>> { + pub(crate) fn list(&self, outpoint: OutPoint) -> Result>> { Ok( self .database .begin_read()? .open_table(OUTPOINT_TO_SAT_RANGES)? - .get(&outpoint)? - .map(|outpoint| outpoint.value().to_vec()), + .get(&outpoint.store())? + .map(|outpoint| outpoint.value().to_vec()) + .map(|sat_ranges| { + sat_ranges + .chunks_exact(11) + .map(|chunk| SatRange::load(chunk.try_into().unwrap())) + .collect::>() + }), ) } - pub(crate) fn list(&self, outpoint: OutPoint) -> Result> { - if !self.index_sats || outpoint == unbound_outpoint() { - return Ok(None); + pub(crate) fn is_output_spent(&self, outpoint: OutPoint) -> Result { + Ok( + outpoint != OutPoint::null() + && outpoint != self.options.chain().genesis_coinbase_outpoint() + && self + .client + .get_tx_out(&outpoint.txid, outpoint.vout, Some(false))? + .is_none(), + ) + } + + pub(crate) fn is_output_in_active_chain(&self, outpoint: OutPoint) -> Result { + if outpoint == OutPoint::null() { + return Ok(true); } - let array = outpoint.store(); + if outpoint == self.options.chain().genesis_coinbase_outpoint() { + return Ok(true); + } - let sat_ranges = self.list_inner(array)?; + let Some(info) = self + .client + .get_raw_transaction_info(&outpoint.txid, None) + .into_option()? + else { + return Ok(false); + }; - match sat_ranges { - Some(sat_ranges) => Ok(Some(List::Unspent( - sat_ranges - .chunks_exact(11) - .map(|chunk| SatRange::load(chunk.try_into().unwrap())) - .collect(), - ))), - None => { - if self.is_transaction_in_active_chain(outpoint.txid)? { - Ok(Some(List::Spent)) - } else { - Ok(None) - } - } + if !info.in_active_chain.unwrap_or_default() { + return Ok(false); + } + + if usize::try_from(outpoint.vout).unwrap() >= info.vout.len() { + return Ok(false); } + + Ok(true) } pub(crate) fn block_time(&self, height: Height) -> Result { @@ -2247,7 +2276,7 @@ mod tests { ) .unwrap() .unwrap(), - List::Unspent(vec![(0, 50 * COIN_VALUE)]) + &[(0, 50 * COIN_VALUE)], ) } @@ -2257,7 +2286,7 @@ mod tests { let txid = context.mine_blocks(1)[0].txdata[0].txid(); assert_eq!( context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(), - List::Unspent(vec![(50 * COIN_VALUE, 100 * COIN_VALUE)]) + &[(50 * COIN_VALUE, 100 * COIN_VALUE)], ) } @@ -2278,12 +2307,12 @@ mod tests { assert_eq!( context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(), - List::Unspent(vec![(50 * COIN_VALUE, 75 * COIN_VALUE)]) + &[(50 * COIN_VALUE, 75 * COIN_VALUE)], ); assert_eq!( context.index.list(OutPoint::new(txid, 1)).unwrap().unwrap(), - List::Unspent(vec![(75 * COIN_VALUE, 100 * COIN_VALUE)]) + &[(75 * COIN_VALUE, 100 * COIN_VALUE)], ); } @@ -2303,10 +2332,10 @@ mod tests { assert_eq!( context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(), - List::Unspent(vec![ + &[ (50 * COIN_VALUE, 100 * COIN_VALUE), (100 * COIN_VALUE, 150 * COIN_VALUE) - ]), + ], ); } @@ -2326,12 +2355,12 @@ mod tests { assert_eq!( context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(), - List::Unspent(vec![(50 * COIN_VALUE, 7499999995)]), + &[(50 * COIN_VALUE, 7499999995)], ); assert_eq!( context.index.list(OutPoint::new(txid, 1)).unwrap().unwrap(), - List::Unspent(vec![(7499999995, 9999999990)]), + &[(7499999995, 9999999990)], ); assert_eq!( @@ -2340,7 +2369,7 @@ mod tests { .list(OutPoint::new(coinbase_txid, 0)) .unwrap() .unwrap(), - List::Unspent(vec![(10000000000, 15000000000), (9999999990, 10000000000)]) + &[(10000000000, 15000000000), (9999999990, 10000000000)], ); } @@ -2370,11 +2399,11 @@ mod tests { .list(OutPoint::new(coinbase_txid, 0)) .unwrap() .unwrap(), - List::Unspent(vec![ + &[ (15000000000, 20000000000), (9999999990, 10000000000), (14999999990, 15000000000) - ]) + ], ); } @@ -2393,7 +2422,7 @@ mod tests { assert_eq!( context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(), - List::Unspent(Vec::new()) + &[], ); } @@ -2420,7 +2449,7 @@ mod tests { assert_eq!( context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(), - List::Unspent(Vec::new()) + &[], ); } @@ -2435,10 +2464,7 @@ mod tests { }); context.mine_blocks(1); let txid = context.rpc_server.tx(1, 0).txid(); - assert_eq!( - context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(), - List::Spent, - ); + assert_matches!(context.index.list(OutPoint::new(txid, 0)).unwrap(), None); } #[test] @@ -3012,10 +3038,7 @@ mod tests { .args(["--index-sats", "--first-inscription-height", "10"]) .build(); - let null_ranges = || match context.index.list(OutPoint::null()).unwrap().unwrap() { - List::Unspent(ranges) => ranges, - _ => panic!(), - }; + let null_ranges = || context.index.list(OutPoint::null()).unwrap().unwrap(); assert!(null_ranges().is_empty()); @@ -5838,4 +5861,187 @@ mod tests { assert_eq!(sat, entry.sat); } } + + #[test] + fn index_spent_sats_retains_spent_sat_range_entries() { + let ranges = { + let context = Context::builder().arg("--index-sats").build(); + + context.mine_blocks(1); + + let outpoint = OutPoint { + txid: context.rpc_server.tx(1, 0).into(), + vout: 0, + }; + + let ranges = context.index.list(outpoint).unwrap().unwrap(); + + assert!(!ranges.is_empty()); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Default::default())], + ..Default::default() + }); + + context.mine_blocks(1); + + assert!(context.index.list(outpoint).unwrap().is_none()); + + ranges + }; + + { + let context = Context::builder() + .arg("--index-sats") + .arg("--index-spent-sats") + .build(); + + context.mine_blocks(1); + + let outpoint = OutPoint { + txid: context.rpc_server.tx(1, 0).into(), + vout: 0, + }; + + let unspent_ranges = context.index.list(outpoint).unwrap().unwrap(); + + assert!(!unspent_ranges.is_empty()); + + assert_eq!(unspent_ranges, ranges); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Default::default())], + ..Default::default() + }); + + context.mine_blocks(1); + + let spent_ranges = context.index.list(outpoint).unwrap().unwrap(); + + assert_eq!(spent_ranges, ranges); + } + } + + #[test] + fn index_spent_sats_implies_index_sats() { + let context = Context::builder().arg("--index-spent-sats").build(); + + context.mine_blocks(1); + + let outpoint = OutPoint { + txid: context.rpc_server.tx(1, 0).into(), + vout: 0, + }; + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Default::default())], + ..Default::default() + }); + + context.mine_blocks(1); + + assert!(context.index.list(outpoint).unwrap().is_some()); + } + + #[test] + fn spent_sats_are_retained_after_flush() { + let context = Context::builder().arg("--index-spent-sats").build(); + + context.mine_blocks(1); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Default::default())], + ..Default::default() + }); + + context.mine_blocks_with_update(1, false); + + let outpoint = OutPoint { txid, vout: 0 }; + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Default::default())], + ..Default::default() + }); + + context.mine_blocks(1); + + assert!(context.index.list(outpoint).unwrap().is_some()); + } + + #[test] + fn is_output_spent() { + let context = Context::builder().build(); + + assert!(!context.index.is_output_spent(OutPoint::null()).unwrap()); + assert!(!context + .index + .is_output_spent(Chain::Mainnet.genesis_coinbase_outpoint()) + .unwrap()); + + context.mine_blocks(1); + + assert!(!context + .index + .is_output_spent(OutPoint { + txid: context.rpc_server.tx(1, 0).txid(), + vout: 0, + }) + .unwrap()); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Default::default())], + ..Default::default() + }); + + context.mine_blocks(1); + + assert!(context + .index + .is_output_spent(OutPoint { + txid: context.rpc_server.tx(1, 0).txid(), + vout: 0, + }) + .unwrap()); + } + + #[test] + fn is_output_in_active_chain() { + let context = Context::builder().build(); + + assert!(context + .index + .is_output_in_active_chain(OutPoint::null()) + .unwrap()); + + assert!(context + .index + .is_output_in_active_chain(Chain::Mainnet.genesis_coinbase_outpoint()) + .unwrap()); + + context.mine_blocks(1); + + assert!(context + .index + .is_output_in_active_chain(OutPoint { + txid: context.rpc_server.tx(1, 0).txid(), + vout: 0, + }) + .unwrap()); + + assert!(!context + .index + .is_output_in_active_chain(OutPoint { + txid: context.rpc_server.tx(1, 0).txid(), + vout: 1, + }) + .unwrap()); + + assert!(!context + .index + .is_output_in_active_chain(OutPoint { + txid: Txid::all_zeros(), + vout: 0, + }) + .unwrap()); + } } diff --git a/src/index/testing.rs b/src/index/testing.rs index 462f07336c..199dfe7974 100644 --- a/src/index/testing.rs +++ b/src/index/testing.rs @@ -82,8 +82,14 @@ impl Context { } pub(crate) fn mine_blocks(&self, n: u64) -> Vec { + self.mine_blocks_with_update(n, true) + } + + pub(crate) fn mine_blocks_with_update(&self, n: u64, update: bool) -> Vec { let blocks = self.rpc_server.mine_blocks(n); - self.index.update().unwrap(); + if update { + self.index.update().unwrap(); + } blocks } diff --git a/src/index/updater.rs b/src/index/updater.rs index 7aa89768b8..bfc10b0577 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -471,16 +471,23 @@ impl<'index> Updater<'_> { for input in &tx.input { let key = input.previous_output.store(); - let sat_ranges = match self.range_cache.remove(&key) { + let sat_ranges = match if index.index_spent_sats { + self.range_cache.get(&key).cloned() + } else { + self.range_cache.remove(&key) + } { Some(sat_ranges) => { self.outputs_cached += 1; sat_ranges } - None => outpoint_to_sat_ranges - .remove(&key)? - .ok_or_else(|| anyhow!("Could not find outpoint {} in index", input.previous_output))? - .value() - .to_vec(), + None => if index.index_spent_sats { + outpoint_to_sat_ranges.get(&key)? + } else { + outpoint_to_sat_ranges.remove(&key)? + } + .ok_or_else(|| anyhow!("Could not find outpoint {} in index", input.previous_output))? + .value() + .to_vec(), }; for chunk in sat_ranges.chunks_exact(11) { diff --git a/src/lib.rs b/src/lib.rs index 27662d0f9e..4bdb117364 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,6 @@ use { deserialize_from_str::DeserializeFromStr, epoch::Epoch, height::Height, - index::List, inscriptions::{media, teleburn, Charm, Media, ParsedEnvelope}, outgoing::Outgoing, representation::Representation, diff --git a/src/options.rs b/src/options.rs index ec6261f0f8..4d91b3181d 100644 --- a/src/options.rs +++ b/src/options.rs @@ -49,6 +49,8 @@ pub struct Options { pub(crate) index_runes: bool, #[arg(long, help = "Track location of all satoshis.")] pub(crate) index_sats: bool, + #[arg(long, help = "Keep sat index entries of spent outputs.")] + pub(crate) index_spent_sats: bool, #[arg(long, help = "Store transactions in index.")] pub(crate) index_transactions: bool, #[arg( diff --git a/src/subcommand/list.rs b/src/subcommand/list.rs index 8de7f8e0ea..d42b570443 100644 --- a/src/subcommand/list.rs +++ b/src/subcommand/list.rs @@ -8,13 +8,18 @@ pub(crate) struct List { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Output { - pub output: OutPoint, - pub start: u64, + pub ranges: Option>, + pub spent: bool, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Range { pub end: u64, - pub size: u64, + pub name: String, pub offset: u64, pub rarity: Rarity, - pub name: String, + pub size: u64, + pub start: u64, } impl List { @@ -27,52 +32,35 @@ impl List { index.update()?; - match index.list(self.outpoint)? { - Some(crate::index::List::Unspent(ranges)) => { - let mut outputs = Vec::new(); - for Output { - output, - start, - end, - size, - offset, - rarity, - name, - } in list(self.outpoint, ranges) - { - outputs.push(Output { - output, - start, - end, - size, - offset, - rarity, - name, - }); - } - - Ok(Some(Box::new(outputs))) - } - Some(crate::index::List::Spent) => Err(anyhow!("output spent.")), - None => Err(anyhow!("output not found")), + ensure! { + index.is_output_in_active_chain(self.outpoint)?, + "output not found" } + + let ranges = index.list(self.outpoint)?; + + let spent = index.is_output_spent(self.outpoint)?; + + Ok(Some(Box::new(Output { + spent, + ranges: ranges.map(output_ranges), + }))) } } -fn list(outpoint: OutPoint, ranges: Vec<(u64, u64)>) -> Vec { +fn output_ranges(ranges: Vec<(u64, u64)>) -> Vec { let mut offset = 0; ranges .into_iter() .map(|(start, end)| { let size = end - start; - let output = Output { - output: outpoint, - start, + let output = Range { end, - size, - offset, name: Sat(start).name(), + offset, rarity: Sat(start).rarity(), + size, + start, }; offset += size; @@ -86,69 +74,39 @@ fn list(outpoint: OutPoint, ranges: Vec<(u64, u64)>) -> Vec { mod tests { use super::*; - fn output( - output: OutPoint, - start: u64, - end: u64, - size: u64, - offset: u64, - rarity: Rarity, - name: String, - ) -> super::Output { - super::Output { - output, - start, - end, - size, - offset, - name, - rarity, - } - } - #[test] fn list_ranges() { - let outpoint = - OutPoint::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:5") - .unwrap(); - let ranges = vec![ - (50 * COIN_VALUE, 55 * COIN_VALUE), - (10, 100), - (1050000000000000, 1150000000000000), - ]; assert_eq!( - list(outpoint, ranges), + output_ranges(vec![ + (50 * COIN_VALUE, 55 * COIN_VALUE), + (10, 100), + (1050000000000000, 1150000000000000), + ]), vec![ - output( - OutPoint::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:5") - .unwrap(), - 50 * COIN_VALUE, - 55 * COIN_VALUE, - 5 * COIN_VALUE, - 0, - Rarity::Uncommon, - "nvtcsezkbth".to_string() - ), - output( - OutPoint::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:5") - .unwrap(), - 10, - 100, - 90, - 5 * COIN_VALUE, - Rarity::Common, - "nvtdijuwxlf".to_string() - ), - output( - OutPoint::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:5") - .unwrap(), - 1050000000000000, - 1150000000000000, - 100000000000000, - 5 * COIN_VALUE + 90, - Rarity::Epic, - "gkjbdrhkfqf".to_string() - ) + Range { + end: 55 * COIN_VALUE, + name: "nvtcsezkbth".to_string(), + offset: 0, + rarity: Rarity::Uncommon, + size: 5 * COIN_VALUE, + start: 50 * COIN_VALUE, + }, + Range { + end: 100, + name: "nvtdijuwxlf".to_string(), + offset: 5 * COIN_VALUE, + rarity: Rarity::Common, + size: 90, + start: 10, + }, + Range { + end: 1150000000000000, + name: "gkjbdrhkfqf".to_string(), + offset: 5 * COIN_VALUE + 90, + rarity: Rarity::Epic, + size: 100000000000000, + start: 1050000000000000, + } ] ) } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index b34b1c4d72..30d7e8ac48 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -545,14 +545,14 @@ impl Server { AcceptJson(accept_json): AcceptJson, ) -> ServerResult { task::block_in_place(|| { - let list = index.list(outpoint)?; + let sat_ranges = index.list(outpoint)?; let indexed; let output = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { let mut value = 0; - if let Some(List::Unspent(ranges)) = &list { + if let Some(ranges) = &sat_ranges { for (start, end) in ranges { value += end - start; } @@ -580,28 +580,32 @@ impl Server { let runes = index.get_rune_balances_for_outpoint(outpoint)?; + let spent = index.is_output_spent(outpoint)?; + Ok(if accept_json { Json(OutputJson::new( - outpoint, - list, server_config.chain, - output, inscriptions, + outpoint, + output, indexed, runes .into_iter() .map(|(spaced_rune, pile)| (spaced_rune.rune, pile.amount)) .collect(), + sat_ranges, + spent, )) .into_response() } else { OutputHtml { - outpoint, - inscriptions, - list, chain: server_config.chain, + inscriptions, + outpoint, output, runes, + sat_ranges, + spent, } .page(server_config) .into_response() @@ -2599,6 +2603,7 @@ mod tests { sat_ranges: None, indexed: true, inscriptions: Vec::new(), + spent: false, runes: vec![(Rune(RUNE), 340282366920938463463374607431768211455)] .into_iter() .collect(), @@ -2858,6 +2863,7 @@ mod tests {
value
5000000000
script pubkey
OP_PUSHBYTES_65 [[:xdigit:]]{{130}} OP_CHECKSIG
transaction
{txid}
+
spent
false

1 Sat Range