From 4ebb84ad026ec8362465f3da098d50188a25dee1 Mon Sep 17 00:00:00 2001 From: liam <31192478+terror@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:52:10 -0400 Subject: [PATCH] Add better message for spent outputs (#345) --- src/index.rs | 56 +++++++++++----- src/main.rs | 20 ++++-- src/purse.rs | 18 ++++-- src/subcommand/list.rs | 3 +- src/subcommand/server.rs | 23 +------ src/subcommand/server/templates/output.rs | 24 ++++++- templates/output.html | 9 ++- tests/lib.rs | 2 +- tests/list.rs | 2 +- tests/server.rs | 79 +++++++++++++++++------ tests/state.rs | 4 +- tests/wallet.rs | 32 +-------- 12 files changed, 168 insertions(+), 104 deletions(-) diff --git a/src/index.rs b/src/index.rs index 837a27d129..47b1bf7374 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1,5 +1,6 @@ use { super::*, + bitcoin::consensus::encode::serialize, bitcoincore_rpc::{Auth, Client, RpcApi}, rayon::iter::{IntoParallelRefIterator, ParallelIterator}, redb::WriteStrategy, @@ -10,6 +11,7 @@ mod rtx; const HEIGHT_TO_HASH: TableDefinition = TableDefinition::new("HEIGHT_TO_HASH"); const OUTPOINT_TO_ORDINAL_RANGES: TableDefinition<[u8], [u8]> = TableDefinition::new("OUTPOINT_TO_ORDINAL_RANGES"); +const OUTPOINT_TO_TXID: TableDefinition<[u8], [u8]> = TableDefinition::new("OUTPOINT_TO_TXID"); pub(crate) struct Index { client: Client, @@ -17,6 +19,11 @@ pub(crate) struct Index { database_path: PathBuf, } +pub(crate) enum List { + Spent(Txid), + Unspent(Vec<(u64, u64)>), +} + impl Index { pub(crate) fn open(options: &Options) -> Result { let rpc_url = options.rpc_url(); @@ -46,6 +53,7 @@ impl Index { tx.open_table(HEIGHT_TO_HASH)?; tx.open_table(OUTPOINT_TO_ORDINAL_RANGES)?; + tx.open_table(OUTPOINT_TO_TXID)?; tx.commit()?; @@ -130,6 +138,7 @@ impl Index { pub(crate) fn index_block(&self, wtx: &mut WriteTransaction) -> Result { let mut height_to_hash = wtx.open_table(HEIGHT_TO_HASH)?; let mut outpoint_to_ordinal_ranges = wtx.open_table(OUTPOINT_TO_ORDINAL_RANGES)?; + let mut outpoint_to_txid = wtx.open_table(OUTPOINT_TO_TXID)?; let start = Instant::now(); let mut ordinal_ranges_written = 0; @@ -188,11 +197,10 @@ impl Index { let mut input_ordinal_ranges = VecDeque::new(); for input in &tx.input { - let mut key = Vec::new(); - input.previous_output.consensus_encode(&mut key)?; + let key = serialize(&input.previous_output); let ordinal_ranges = outpoint_to_ordinal_ranges - .get(key.as_slice())? + .get(&key)? .ok_or_else(|| anyhow!("Could not find outpoint in index"))?; for chunk in ordinal_ranges.chunks_exact(11) { @@ -206,6 +214,7 @@ impl Index { *txid, tx, &mut outpoint_to_ordinal_ranges, + &mut outpoint_to_txid, &mut input_ordinal_ranges, &mut ordinal_ranges_written, )?; @@ -218,6 +227,7 @@ impl Index { *txid, tx, &mut outpoint_to_ordinal_ranges, + &mut outpoint_to_txid, &mut coinbase_inputs, &mut ordinal_ranges_written, )?; @@ -266,6 +276,7 @@ impl Index { txid: Txid, tx: &Transaction, outpoint_to_ordinal_ranges: &mut Table<[u8], [u8]>, + outpoint_to_txid: &mut Table<[u8], [u8]>, input_ordinal_ranges: &mut VecDeque<(u64, u64)>, ordinal_ranges_written: &mut u64, ) -> Result { @@ -304,9 +315,11 @@ impl Index { *ordinal_ranges_written += 1; } - let mut outpoint_encoded = Vec::new(); - outpoint.consensus_encode(&mut outpoint_encoded)?; - outpoint_to_ordinal_ranges.insert(&outpoint_encoded, &ordinals)?; + outpoint_to_ordinal_ranges.insert(&serialize(&outpoint), &ordinals)?; + } + + for input in &tx.input { + outpoint_to_txid.insert(&serialize(&input.previous_output), &txid)?; } Ok(()) @@ -396,19 +409,28 @@ impl Index { ) } - pub(crate) fn list(&self, outpoint: OutPoint) -> Result>> { - let mut outpoint_encoded = Vec::new(); - outpoint.consensus_encode(&mut outpoint_encoded)?; + pub(crate) fn list(&self, outpoint: OutPoint) -> Result> { + let outpoint_encoded = serialize(&outpoint); + let ordinal_ranges = self.list_inner(&outpoint_encoded)?; + match ordinal_ranges { - Some(ordinal_ranges) => { - let mut output = Vec::new(); - for chunk in ordinal_ranges.chunks_exact(11) { - output.push(Self::decode_ordinal_range(chunk.try_into().unwrap())); - } - Ok(Some(output)) - } - None => Ok(None), + Some(ordinal_ranges) => Ok(Some(List::Unspent( + ordinal_ranges + .chunks_exact(11) + .map(|chunk| Self::decode_ordinal_range(chunk.try_into().unwrap())) + .collect(), + ))), + None => Ok( + self + .database + .begin_read()? + .open_table(OUTPOINT_TO_TXID)? + .get(&outpoint_encoded)? + .map(Txid::consensus_decode) + .transpose()? + .map(List::Spent), + ), } } diff --git a/src/main.rs b/src/main.rs index 348234bdb3..da4ca54681 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,14 +2,22 @@ use { self::{ - arguments::Arguments, blocktime::Blocktime, bytes::Bytes, degree::Degree, epoch::Epoch, - height::Height, index::Index, nft::Nft, options::Options, ordinal::Ordinal, purse::Purse, - sat_point::SatPoint, subcommand::Subcommand, + arguments::Arguments, + blocktime::Blocktime, + bytes::Bytes, + degree::Degree, + epoch::Epoch, + height::Height, + index::{Index, List}, + nft::Nft, + options::Options, + ordinal::Ordinal, + purse::Purse, + sat_point::SatPoint, + subcommand::Subcommand, }, anyhow::{anyhow, bail, Context, Error}, - axum::{ - extract, http::StatusCode, response::Html, response::IntoResponse, routing::get, Json, Router, - }, + axum::{extract, http::StatusCode, response::Html, response::IntoResponse, routing::get, Router}, axum_server::Handle, bdk::{ blockchain::rpc::{Auth, RpcBlockchain, RpcConfig}, diff --git a/src/purse.rs b/src/purse.rs index 006e35f74e..0c545157cb 100644 --- a/src/purse.rs +++ b/src/purse.rs @@ -73,12 +73,22 @@ impl Purse { let index = Index::index(options)?; for utxo in self.wallet.list_unspent()? { - if let Some(ranges) = index.list(utxo.outpoint)? { - for (start, end) in ranges { - if ordinal.0 >= start && ordinal.0 < end { - return Ok(utxo); + match index.list(utxo.outpoint)? { + Some(List::Unspent(ranges)) => { + for (start, end) in ranges { + if ordinal.0 >= start && ordinal.0 < end { + return Ok(utxo); + } } } + Some(List::Spent(txid)) => { + return Err(anyhow!( + "UTXO unspent in wallet but spent in index by transaction {txid}" + )); + } + None => { + return Err(anyhow!("UTXO unspent in wallet but not found in index")); + } } } diff --git a/src/subcommand/list.rs b/src/subcommand/list.rs index f34f6525f2..28657485cb 100644 --- a/src/subcommand/list.rs +++ b/src/subcommand/list.rs @@ -10,12 +10,13 @@ impl List { let index = Index::index(&options)?; match index.list(self.outpoint)? { - Some(ranges) => { + Some(crate::index::List::Unspent(ranges)) => { for (start, end) in ranges { println!("[{start},{end})"); } Ok(()) } + Some(crate::index::List::Spent(txid)) => Err(anyhow!("Output spent in transaction {txid}")), None => Err(anyhow!("Output not found")), } } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index fc8f711b73..471eded702 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -95,7 +95,6 @@ impl Server { let app = Router::new() .route("/", get(Self::home)) - .route("/api/list/:outpoint", get(Self::api_list)) .route("/block/:hash", get(Self::block)) .route("/bounties", get(Self::bounties)) .route("/faq", get(Self::faq)) @@ -203,12 +202,8 @@ impl Server { extract::Path(outpoint): extract::Path, ) -> impl IntoResponse { match index.list(outpoint) { - Ok(Some(ranges)) => OutputHtml { outpoint, ranges }.page().into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Html("Output unknown, invalid, or spent.".to_string()), - ) - .into_response(), + Ok(Some(list)) => OutputHtml { outpoint, list }.page().into_response(), + Ok(None) => (StatusCode::NOT_FOUND, Html("Output unknown.".to_string())).into_response(), Err(err) => { eprintln!("Error serving request for output: {err}"); ( @@ -324,20 +319,6 @@ impl Server { } } - async fn api_list( - extract::Path(outpoint): extract::Path, - index: extract::Extension>, - ) -> impl IntoResponse { - match index.list(outpoint) { - Ok(Some(ranges)) => (StatusCode::OK, Json(Some(ranges))), - Ok(None) => (StatusCode::NOT_FOUND, Json(None)), - Err(error) => { - eprintln!("Error serving request for outpoint {outpoint}: {error}"); - (StatusCode::INTERNAL_SERVER_ERROR, Json(None)) - } - } - } - async fn status() -> impl IntoResponse { ( StatusCode::OK, diff --git a/src/subcommand/server/templates/output.rs b/src/subcommand/server/templates/output.rs index c6a335b865..7908b8c85d 100644 --- a/src/subcommand/server/templates/output.rs +++ b/src/subcommand/server/templates/output.rs @@ -3,7 +3,7 @@ use super::*; #[derive(Display)] pub(crate) struct OutputHtml { pub(crate) outpoint: OutPoint, - pub(crate) ranges: Vec<(u64, u64)>, + pub(crate) list: List, } impl Content for OutputHtml { @@ -17,13 +17,13 @@ mod tests { use {super::*, pretty_assertions::assert_eq, unindent::Unindent}; #[test] - fn output_html() { + fn unspent_output() { assert_eq!( OutputHtml { outpoint: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0" .parse() .unwrap(), - ranges: vec![(0, 1), (1, 2)] + list: List::Unspent(vec![(0, 1), (1, 2)]) } .to_string(), " @@ -37,4 +37,22 @@ mod tests { .unindent() ); } + + #[test] + fn spent_output() { + assert_eq!( + OutputHtml { + outpoint: "0000000000000000000000000000000000000000000000000000000000000000:0" + .parse() + .unwrap(), + list: List::Spent("1111111111111111111111111111111111111111111111111111111111111111".parse().unwrap()) + } + .to_string(), + " +

Output 0000000000000000000000000000000000000000000000000000000000000000:0

+

Spent by transaction 1111111111111111111111111111111111111111111111111111111111111111.

+ " + .unindent() + ); + } } diff --git a/templates/output.html b/templates/output.html index ede2e426ea..6b6dc39f18 100644 --- a/templates/output.html +++ b/templates/output.html @@ -1,7 +1,14 @@

Output {{self.outpoint}}

+%% match &self.list { +%% List::Unspent(ranges) => {

Ordinal Ranges

    -%% for (start, end) in &self.ranges { +%% for (start, end) in ranges {
  • [{{start}},{{end}})
  • %% }
+%% } +%% List::Spent(txid) => { +

Spent by transaction {{ txid }}.

+%% } +%% } diff --git a/tests/lib.rs b/tests/lib.rs index 32c2027185..7f6a304f79 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -13,7 +13,7 @@ use { wallet::{signer::SignOptions, AddressIndex, SyncOptions, Wallet}, KeychainKind, }, - bitcoin::{hash_types::Txid, network::constants::Network, Address, Block, OutPoint}, + bitcoin::{hash_types::Txid, network::constants::Network, Address, Block, OutPoint, Transaction}, bitcoincore_rpc::{Client, RawTx, RpcApi}, executable_path::executable_path, log::LevelFilter, diff --git a/tests/list.rs b/tests/list.rs index 5b0324db6d..2b1bb2db98 100644 --- a/tests/list.rs +++ b/tests/list.rs @@ -172,7 +172,7 @@ fn old_transactions_are_pruned() { fee: 50 * 100_000_000, }) .blocks(1) - .expected_stderr("error: Output not found\n") + .expected_stderr("error: Output spent in transaction 3dbc87de25bf5a52ddfa8038bda36e09622f4dec7951d81ac43e4b0e8c54bc5b\n") .expected_status(1) .run() } diff --git a/tests/server.rs b/tests/server.rs index c0236c1b01..456582d3e5 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -1,18 +1,5 @@ use super::*; -#[test] -fn list() { - let mut state = State::new(); - - state.blocks(1); - - state.request( - "api/list/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0", - 200, - "[[0,5000000000]]", - ); -} - #[test] fn status() { State::new().request("status", 200, "OK"); @@ -135,15 +122,71 @@ fn output() { } #[test] -fn invalid_vout_returns_404() { +fn unknown_output_returns_404() { let mut state = State::new(); - state.blocks(1); - state.request( - "output/0396bc915f141f7de025f72ae9b6bb8dcdb5f444fc245d8fac486ba67a38eef8:0", + "output/0000000000000000000000000000000000000000000000000000000000000000:0", 404, - "Output unknown, invalid, or spent.", + "Output unknown.", + ); +} + +#[test] +fn spent_output_returns_200() { + let mut state = State::new(); + + state.blocks(101); + + let txid = state + .transaction(TransactionOptions { + slots: &[(1, 0, 0)], + output_count: 1, + fee: 0, + }) + .txid(); + + state.blocks(1); + + state.request_regex( + &format!("output/{txid}:0"), + 200, + &format!( + ".*Output {txid}:0.*

Output {txid}:0

+

Ordinal Ranges

+.*" + ), + ); + + let transaction = state.transaction(TransactionOptions { + slots: &[(102, 1, 0)], + output_count: 1, + fee: 0, + }); + + state.blocks(1); + + state.request_regex( + &format!("output/{txid}:0"), + 200, + &format!( + ".*

Spent by transaction {}.

.*", + transaction.txid(), + transaction.txid() + ), + ); +} + +#[test] +fn invalid_output_returns_400() { + let mut state = State::new(); + + state.request_regex( + "output/foo:0", + 400, + "Invalid URL: error parsing TXID: odd hex string length 3", ); } diff --git a/tests/state.rs b/tests/state.rs index fdbb9c2cbf..8afafd8934 100644 --- a/tests/state.rs +++ b/tests/state.rs @@ -139,7 +139,7 @@ impl State { .unwrap() } - pub(crate) fn transaction(&self, options: TransactionOptions) { + pub(crate) fn transaction(&self, options: TransactionOptions) -> Transaction { self.sync(); let input_value = options @@ -197,6 +197,8 @@ impl State { &[tx.raw_hex().into(), 21000000.into()], ) .unwrap(); + + tx } pub(crate) fn request(&mut self, path: &str, status: u16, expected_response: &str) { diff --git a/tests/wallet.rs b/tests/wallet.rs index 281132bdab..43d6e78e81 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -227,26 +227,12 @@ fn send_owned_ordinal() { ) .unwrap(); - let mut output = Test::with_state(output.state) + let output = Test::with_state(output.state) .command("--network regtest wallet utxos") .expected_status(0) .stdout_regex("[[:xdigit:]]{64}:[[:digit:]] 5000000000\n") .output(); - output.state.request( - &format!( - "api/list/{}", - output - .stdout - .split(' ') - .collect::>() - .first() - .unwrap() - ), - 200, - "[[5000000000,10000000000]]", - ); - let wallet = Wallet::new( Bip84( ( @@ -320,26 +306,12 @@ fn send_foreign_ordinal() { .generate_to_address(1, &from_address) .unwrap(); - let mut output = Test::with_state(output.state) + let output = Test::with_state(output.state) .command("--network regtest wallet utxos") .expected_status(0) .stdout_regex("[[:xdigit:]]{64}:[[:digit:]] 5000000000\n") .output(); - output.state.request( - &format!( - "api/list/{}", - output - .stdout - .split(' ') - .collect::>() - .first() - .unwrap() - ), - 200, - "[[5000000000,10000000000]]", - ); - let wallet = Wallet::new( Bip84( (