Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add better errors for outputs #345

Merged
merged 11 commits into from
Aug 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 39 additions & 17 deletions src/index.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use {
super::*,
bitcoin::consensus::encode::serialize,
bitcoincore_rpc::{Auth, Client, RpcApi},
rayon::iter::{IntoParallelRefIterator, ParallelIterator},
redb::WriteStrategy,
Expand All @@ -10,13 +11,19 @@ mod rtx;
const HEIGHT_TO_HASH: TableDefinition<u64, [u8]> = 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,
database: Database,
database_path: PathBuf,
}

pub(crate) enum List {
Spent(Txid),
Unspent(Vec<(u64, u64)>),
}

impl Index {
pub(crate) fn open(options: &Options) -> Result<Self> {
let rpc_url = options.rpc_url();
Expand Down Expand Up @@ -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()?;

Expand Down Expand Up @@ -130,6 +138,7 @@ impl Index {
pub(crate) fn index_block(&self, wtx: &mut WriteTransaction) -> Result<bool> {
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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
)?;
Expand All @@ -218,6 +227,7 @@ impl Index {
*txid,
tx,
&mut outpoint_to_ordinal_ranges,
&mut outpoint_to_txid,
&mut coinbase_inputs,
&mut ordinal_ranges_written,
)?;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(())
Expand Down Expand Up @@ -396,19 +409,28 @@ impl Index {
)
}

pub(crate) fn list(&self, outpoint: OutPoint) -> Result<Option<Vec<(u64, u64)>>> {
let mut outpoint_encoded = Vec::new();
outpoint.consensus_encode(&mut outpoint_encoded)?;
pub(crate) fn list(&self, outpoint: OutPoint) -> Result<Option<List>> {
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),
),
}
}

Expand Down
20 changes: 14 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
18 changes: 14 additions & 4 deletions src/purse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/subcommand/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
}
}
Expand Down
23 changes: 2 additions & 21 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -203,12 +202,8 @@ impl Server {
extract::Path(outpoint): extract::Path<OutPoint>,
) -> 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}");
(
Expand Down Expand Up @@ -324,20 +319,6 @@ impl Server {
}
}

async fn api_list(
extract::Path(outpoint): extract::Path<OutPoint>,
index: extract::Extension<Arc<Index>>,
) -> 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,
Expand Down
24 changes: 21 additions & 3 deletions src/subcommand/server/templates/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(),
"
Expand All @@ -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(),
"
<h1>Output 0000000000000000000000000000000000000000000000000000000000000000:0</h1>
<p>Spent by transaction <a href=/tx/1111111111111111111111111111111111111111111111111111111111111111>1111111111111111111111111111111111111111111111111111111111111111</a>.</p>
"
.unindent()
);
}
}
9 changes: 8 additions & 1 deletion templates/output.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
<h1>Output {{self.outpoint}}</h1>
%% match &self.list {
%% List::Unspent(ranges) => {
<h2>Ordinal Ranges</h2>
<ul>
%% for (start, end) in &self.ranges {
%% for (start, end) in ranges {
<li><a href=/range/{{start}}/{{end}}>[{{start}},{{end}})</a></li>
%% }
</ul>
%% }
%% List::Spent(txid) => {
<p>Spent by transaction <a href=/tx/{{ txid }}>{{ txid }}</a>.</p>
%% }
%% }
2 changes: 1 addition & 1 deletion tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tests/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Loading