Skip to content

Commit

Permalink
Show message when output couldn't be listed because it was spent (#585)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Sep 30, 2022
1 parent a1ede2e commit cae51b1
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 56 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
ci: clippy forbid
cargo fmt -- --check
cargo test -- --test-threads=1
cargo test --all

forbid:
./bin/forbid
Expand Down
92 changes: 54 additions & 38 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ mod rtx;
const HEIGHT_TO_HASH: TableDefinition<u64, [u8; 32]> = TableDefinition::new("HEIGHT_TO_HASH");
const OUTPOINT_TO_ORDINAL_RANGES: TableDefinition<[u8; 36], [u8]> =
TableDefinition::new("OUTPOINT_TO_ORDINAL_RANGES");
const OUTPOINT_TO_TXID: TableDefinition<[u8; 36], [u8; 32]> =
TableDefinition::new("OUTPOINT_TO_TXID");
const STATISTICS: TableDefinition<u64, u64> = TableDefinition::new("STATISTICS");

pub(crate) struct Index {
Expand All @@ -27,7 +25,7 @@ pub(crate) struct Index {

#[derive(Debug, PartialEq)]
pub(crate) enum List {
Spent(Txid),
Spent,
Unspent(Vec<(u64, u64)>),
}

Expand Down Expand Up @@ -101,7 +99,6 @@ impl Index {

tx.open_table(HEIGHT_TO_HASH)?;
tx.open_table(OUTPOINT_TO_ORDINAL_RANGES)?;
tx.open_table(OUTPOINT_TO_TXID)?;
tx.open_table(STATISTICS)?;

tx.commit()?;
Expand Down Expand Up @@ -209,7 +206,6 @@ impl Index {
pub(crate) fn index_block(&self, wtx: &mut WriteTransaction, height: u64) -> 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 mut statistics = wtx.open_table(STATISTICS)?;

let start = Instant::now();
Expand Down Expand Up @@ -299,7 +295,6 @@ impl Index {
*txid,
tx,
&mut outpoint_to_ordinal_ranges,
&mut outpoint_to_txid,
&mut input_ordinal_ranges,
&mut ordinal_ranges_written,
&mut outputs_in_block,
Expand All @@ -313,7 +308,6 @@ impl Index {
*txid,
tx,
&mut outpoint_to_ordinal_ranges,
&mut outpoint_to_txid,
&mut coinbase_inputs,
&mut ordinal_ranges_written,
&mut outputs_in_block,
Expand Down Expand Up @@ -371,7 +365,6 @@ impl Index {
txid: Txid,
tx: &Transaction,
outpoint_to_ordinal_ranges: &mut Table<[u8; 36], [u8]>,
#[allow(unused)] outpoint_to_txid: &mut Table<[u8; 36], [u8; 32]>,
input_ordinal_ranges: &mut VecDeque<(u64, u64)>,
ordinal_ranges_written: &mut u64,
outputs_traversed: &mut u64,
Expand Down Expand Up @@ -416,11 +409,6 @@ impl Index {
outpoint_to_ordinal_ranges.insert(&serialize(&outpoint).try_into().unwrap(), &ordinals)?;
}

#[cfg(any())]
for input in &tx.input {
outpoint_to_txid.insert(&serialize(&input.previous_output), &txid)?;
}

Ok(())
}

Expand All @@ -447,6 +435,21 @@ impl Index {
self.client.get_raw_transaction(&txid, None).into_option()
}

pub(crate) fn is_transaction_in_active_chain(&self, txid: Txid) -> Result<bool> {
Ok(
self
.client
.get_raw_transaction_info(&txid, None)
.into_option()?
.and_then(|transaction_info| {
transaction_info
.confirmations
.map(|confirmations| confirmations > 0)
})
.unwrap_or(false),
)
}

pub(crate) fn find(&self, ordinal: u64) -> Result<Option<SatPoint>> {
if self.height()? < Ordinal(ordinal).height() {
return Ok(None);
Expand Down Expand Up @@ -499,16 +502,13 @@ impl Index {
.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.try_into().unwrap())?
.map(|txid| deserialize(txid.as_slice()))
.transpose()?
.map(List::Spent),
),
None => {
if self.is_transaction_in_active_chain(outpoint.txid)? {
Ok(Some(List::Spent))
} else {
Ok(None)
}
}
}
}

Expand Down Expand Up @@ -640,7 +640,7 @@ mod tests {
let context = Context::new();

context.rpc_server.mine_blocks(1);
let split_coinbase_output = test_bitcoincore_rpc::TransactionTemplate {
let split_coinbase_output = TransactionTemplate {
input_slots: &[(1, 0, 0)],
output_count: 2,
fee: 0,
Expand All @@ -666,7 +666,7 @@ mod tests {
let context = Context::new();

context.rpc_server.mine_blocks(2);
let merge_coinbase_outputs = test_bitcoincore_rpc::TransactionTemplate {
let merge_coinbase_outputs = TransactionTemplate {
input_slots: &[(1, 0, 0), (2, 0, 0)],
output_count: 1,
fee: 0,
Expand All @@ -690,7 +690,7 @@ mod tests {
let context = Context::new();

context.rpc_server.mine_blocks(1);
let fee_paying_tx = test_bitcoincore_rpc::TransactionTemplate {
let fee_paying_tx = TransactionTemplate {
input_slots: &[(1, 0, 0)],
output_count: 2,
fee: 10,
Expand Down Expand Up @@ -724,12 +724,12 @@ mod tests {
let context = Context::new();

context.rpc_server.mine_blocks(2);
let first_fee_paying_tx = test_bitcoincore_rpc::TransactionTemplate {
let first_fee_paying_tx = TransactionTemplate {
input_slots: &[(1, 0, 0)],
output_count: 1,
fee: 10,
};
let second_fee_paying_tx = test_bitcoincore_rpc::TransactionTemplate {
let second_fee_paying_tx = TransactionTemplate {
input_slots: &[(2, 0, 0)],
output_count: 1,
fee: 10,
Expand Down Expand Up @@ -759,7 +759,7 @@ mod tests {
let context = Context::new();

context.rpc_server.mine_blocks(1);
let no_value_output = test_bitcoincore_rpc::TransactionTemplate {
let no_value_output = TransactionTemplate {
input_slots: &[(1, 0, 0)],
output_count: 1,
fee: 50 * COIN_VALUE,
Expand All @@ -779,15 +779,15 @@ mod tests {
let context = Context::new();

context.rpc_server.mine_blocks(1);
let no_value_output = test_bitcoincore_rpc::TransactionTemplate {
let no_value_output = TransactionTemplate {
input_slots: &[(1, 0, 0)],
output_count: 1,
fee: 50 * COIN_VALUE,
};
context.rpc_server.broadcast_tx(no_value_output);
context.rpc_server.mine_blocks(1);

let no_value_input = test_bitcoincore_rpc::TransactionTemplate {
let no_value_input = TransactionTemplate {
input_slots: &[(2, 1, 0)],
output_count: 1,
fee: 0,
Expand All @@ -802,6 +802,24 @@ mod tests {
);
}

#[test]
fn list_spent_output() {
let context = Context::new();
context.rpc_server.mine_blocks(1);
context.rpc_server.broadcast_tx(TransactionTemplate {
input_slots: &[(1, 0, 0)],
output_count: 1,
fee: 0,
});
context.rpc_server.mine_blocks(1);
context.index.index().unwrap();
let txid = context.rpc_server.tx(1, 0).txid();
assert_eq!(
context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(),
List::Spent,
);
}

#[test]
fn list_unknown_output() {
let context = Context::new();
Expand Down Expand Up @@ -873,13 +891,11 @@ mod tests {
fn find_first_satoshi_spent_in_second_block() {
let context = Context::new();
context.rpc_server.mine_blocks(1);
let spend_txid = context
.rpc_server
.broadcast_tx(test_bitcoincore_rpc::TransactionTemplate {
input_slots: &[(1, 0, 0)],
output_count: 1,
fee: 0,
});
let spend_txid = context.rpc_server.broadcast_tx(TransactionTemplate {
input_slots: &[(1, 0, 0)],
output_count: 1,
fee: 0,
});
context.rpc_server.mine_blocks(1);
context.index.index().unwrap();
assert_eq!(
Expand Down
2 changes: 1 addition & 1 deletion src/subcommand/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl List {
}
Ok(())
}
Some(crate::index::List::Spent(txid)) => Err(anyhow!("Output spent in transaction {txid}")),
Some(crate::index::List::Spent) => Err(anyhow!("Output spent.")),
None => Err(anyhow!("Output not found")),
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1096,7 +1096,7 @@ mod tests {
let test_server = TestServer::new();

test_server.bitcoin_rpc_server.mine_blocks(1);
let transaction = test_bitcoincore_rpc::TransactionTemplate {
let transaction = TransactionTemplate {
input_slots: &[(1, 0, 0)],
output_count: 1,
fee: 0,
Expand Down
4 changes: 2 additions & 2 deletions src/subcommand/server/templates/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ mod tests {
outpoint: "0000000000000000000000000000000000000000000000000000000000000000:0"
.parse()
.unwrap(),
list: List::Spent("1111111111111111111111111111111111111111111111111111111111111111".parse().unwrap())
list: List::Spent,
}
.to_string(),
"
<h1>Output 0000000000000000000000000000000000000000000000000000000000000000:0</h1>
<p>Spent by transaction <a href=/tx/1111111111111111111111111111111111111111111111111111111111111111>1111111111111111111111111111111111111111111111111111111111111111</a>.</p>
<p>Output has been spent.</p>
"
.unindent()
);
Expand Down
2 changes: 1 addition & 1 deletion src/test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub(crate) use tempfile::TempDir;
pub(crate) use {tempfile::TempDir, test_bitcoincore_rpc::TransactionTemplate};

macro_rules! assert_regex_match {
($string:expr, $pattern:expr $(,)?) => {
Expand Down
4 changes: 2 additions & 2 deletions templates/output.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ <h2>Ordinal Ranges</h2>
%% }
</ul>
%% }
%% List::Spent(txid) => {
<p>Spent by transaction <a href=/tx/{{ txid }}>{{ txid }}</a>.</p>
%% List::Spent => {
<p>Output has been spent.</p>
%% }
%% }
2 changes: 2 additions & 0 deletions test-bitcoincore-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ repository = "https://github.com/casey/ord"

[dependencies]
bitcoin = { version = "0.29.1", features = ["serde"] }
bitcoincore-rpc-json = "0.16.0"
hex = "0.4.3"
jsonrpc-core = "18.0.0"
jsonrpc-core-client = "18.0.0"
jsonrpc-derive = "18.0.0"
jsonrpc-http-server = "18.0.0"
serde_json = "1.0.0"
52 changes: 42 additions & 10 deletions test-bitcoincore-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use {
bitcoin::{
blockdata::constants::COIN_VALUE, blockdata::script, consensus::encode::serialize,
hash_types::BlockHash, hashes::Hash, Block, BlockHeader, Network, OutPoint, PackedLockTime,
Script, Sequence, Transaction, TxIn, TxMerkleNode, TxOut, Txid, Witness,
Script, Sequence, Transaction, TxIn, TxMerkleNode, TxOut, Txid, Witness, Wtxid,
},
jsonrpc_core::IoHandler,
bitcoincore_rpc_json::GetRawTransactionResult,
jsonrpc_core::{IoHandler, Value},
jsonrpc_http_server::{CloseHandle, ServerBuilder},
std::collections::BTreeMap,
std::{
Expand Down Expand Up @@ -203,7 +204,7 @@ pub trait Api {
txid: Txid,
verbose: bool,
blockhash: Option<BlockHash>,
) -> Result<String, jsonrpc_core::Error>;
) -> Result<Value, jsonrpc_core::Error>;
}

impl Api for Server {
Expand Down Expand Up @@ -245,14 +246,40 @@ impl Api for Server {
txid: Txid,
verbose: bool,
blockhash: Option<BlockHash>,
) -> Result<String, jsonrpc_core::Error> {
assert!(!verbose, "Verbose param is unsupported");
) -> Result<Value, jsonrpc_core::Error> {
assert_eq!(blockhash, None, "Blockhash param is unsupported");
match self.state.lock().unwrap().transactions.get(&txid) {
Some(tx) => Ok(hex::encode(serialize(tx))),
None => Err(jsonrpc_core::Error::new(
jsonrpc_core::types::error::ErrorCode::ServerError(-8),
)),
if verbose {
match self.state.lock().unwrap().transactions.get(&txid) {
Some(_) => Ok(
serde_json::to_value(GetRawTransactionResult {
in_active_chain: None,
hex: Vec::new(),
txid: Txid::all_zeros(),
hash: Wtxid::all_zeros(),
size: 0,
vsize: 0,
version: 0,
locktime: 0,
vin: Vec::new(),
vout: Vec::new(),
blockhash: None,
confirmations: Some(1),
time: None,
blocktime: None,
})
.unwrap(),
),
None => Err(jsonrpc_core::Error::new(
jsonrpc_core::types::error::ErrorCode::ServerError(-8),
)),
}
} else {
match self.state.lock().unwrap().transactions.get(&txid) {
Some(tx) => Ok(Value::String(hex::encode(serialize(tx)))),
None => Err(jsonrpc_core::Error::new(
jsonrpc_core::types::error::ErrorCode::ServerError(-8),
)),
}
}
}
}
Expand Down Expand Up @@ -280,6 +307,11 @@ impl Handle {
pub fn invalidate_tip(&self) -> BlockHash {
self.state.lock().unwrap().pop_block()
}

pub fn tx(&self, bi: usize, ti: usize) -> Transaction {
let state = self.state.lock().unwrap();
state.blocks[&state.hashes[bi]].txdata[ti].clone()
}
}

impl Drop for Handle {
Expand Down

0 comments on commit cae51b1

Please sign in to comment.