Skip to content

Commit

Permalink
Add better message for spent outputs (#345)
Browse files Browse the repository at this point in the history
  • Loading branch information
terror committed Aug 18, 2022
1 parent 1558046 commit 4ebb84a
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 104 deletions.
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

0 comments on commit 4ebb84a

Please sign in to comment.