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
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,
@@ -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();
@@ -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<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;
@@ -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<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),
),
}
}

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

3 changes: 2 additions & 1 deletion src/subcommand/list.rs
Original file line number Diff line number Diff line change
@@ -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")),
}
}
23 changes: 2 additions & 21 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
@@ -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<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}");
(
@@ -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,
24 changes: 21 additions & 3 deletions src/subcommand/server/templates/output.rs
Original file line number Diff line number Diff line change
@@ -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(),
"
<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
@@ -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,
2 changes: 1 addition & 1 deletion tests/list.rs
Original file line number Diff line number Diff line change
@@ -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