Skip to content

Commit e1109d6

Browse files
committed
added http endpoint for transaction indexing
1 parent b9b18f5 commit e1109d6

File tree

14 files changed

+453
-17
lines changed

14 files changed

+453
-17
lines changed

stackslib/src/chainstate/nakamoto/coordinator/tests.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,7 +1429,7 @@ fn pox_treatment() {
14291429
false
14301430
},
14311431
);
1432-
let processing_result = peer.try_process_block(&invalid_block, false).unwrap_err();
1432+
let processing_result = peer.try_process_block(&invalid_block).unwrap_err();
14331433
assert_eq!(
14341434
processing_result.to_string(),
14351435
"Bitvec does not match the block commit's PoX handling".to_string(),
@@ -1520,7 +1520,7 @@ fn pox_treatment() {
15201520
false
15211521
},
15221522
);
1523-
let processing_result = peer.try_process_block(&invalid_block, false).unwrap_err();
1523+
let processing_result = peer.try_process_block(&invalid_block).unwrap_err();
15241524
assert_eq!(
15251525
processing_result.to_string(),
15261526
"Bitvec does not match the block commit's PoX handling".to_string(),
@@ -1614,18 +1614,15 @@ fn transactions_indexing() {
16141614
let (tracked_block, burn_height, ..) =
16151615
peer.single_block_tenure(&private_key, |_| {}, |_| {}, |_| false);
16161616

1617-
assert_eq!(peer.try_process_block(&tracked_block, true).unwrap(), true);
1617+
assert_eq!(peer.try_process_block(&tracked_block).unwrap(), true);
16181618

16191619
let tracked_block_id = tracked_block.block_id();
16201620

16211621
// generate a new block but without txindex
16221622
let (untracked_block, burn_height, ..) =
16231623
peer.single_block_tenure(&private_key, |_| {}, |_| {}, |_| false);
16241624

1625-
assert_eq!(
1626-
peer.try_process_block(&untracked_block, false).unwrap(),
1627-
true
1628-
);
1625+
assert_eq!(peer.try_process_block(&untracked_block).unwrap(), true);
16291626

16301627
let untracked_block_id = untracked_block.block_id();
16311628

stackslib/src/chainstate/nakamoto/tests/node.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,11 +1377,7 @@ impl TestPeer<'_> {
13771377
proof
13781378
}
13791379

1380-
pub fn try_process_block(
1381-
&mut self,
1382-
block: &NakamotoBlock,
1383-
txindex: bool,
1384-
) -> Result<bool, ChainstateError> {
1380+
pub fn try_process_block(&mut self, block: &NakamotoBlock) -> Result<bool, ChainstateError> {
13851381
let mut sort_handle = self.sortdb.as_ref().unwrap().index_handle_at_tip();
13861382
let stacks_tip = sort_handle.get_nakamoto_tip_block_id().unwrap().unwrap();
13871383
let accepted = Relayer::process_new_nakamoto_block(
@@ -1404,7 +1400,7 @@ impl TestPeer<'_> {
14041400
self.sortdb.as_mut().unwrap(),
14051401
&sort_tip,
14061402
None,
1407-
txindex,
1403+
self.config.txindex,
14081404
)?
14091405
else {
14101406
return Ok(false);
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2+
// Copyright (C) 2020-2025 Stacks Open Internet Foundation
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
use std::borrow::BorrowMut;
18+
use std::io::{Read, Write};
19+
20+
use regex::{Captures, Regex};
21+
use stacks_common::codec::StacksMessageCodec;
22+
use stacks_common::types::chainstate::{
23+
BlockHeaderHash, ConsensusHash, StacksBlockId, StacksPublicKey,
24+
};
25+
use stacks_common::types::net::PeerHost;
26+
use stacks_common::types::StacksPublicKeyBuffer;
27+
use stacks_common::util::hash::{to_hex, Hash160, Sha256Sum};
28+
29+
use crate::burnchains::affirmation::AffirmationMap;
30+
use crate::burnchains::Txid;
31+
use crate::chainstate::burn::db::sortdb::SortitionDB;
32+
use crate::chainstate::nakamoto::NakamotoChainState;
33+
use crate::chainstate::stacks::db::StacksChainState;
34+
use crate::core::mempool::MemPoolDB;
35+
use crate::net::http::{
36+
parse_json, Error, HttpNotFound, HttpNotImplemented, HttpRequest, HttpRequestContents,
37+
HttpRequestPreamble, HttpResponse, HttpResponseContents, HttpResponsePayload,
38+
HttpResponsePreamble, HttpServerError,
39+
};
40+
use crate::net::httpcore::{
41+
request, HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler,
42+
StacksHttpRequest, StacksHttpResponse,
43+
};
44+
use crate::net::p2p::PeerNetwork;
45+
use crate::net::{Error as NetError, StacksNodeState, TipRequest};
46+
47+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48+
pub struct TransactionResponse {
49+
pub index_block_hash: StacksBlockId,
50+
pub tx: String,
51+
}
52+
53+
#[derive(Clone)]
54+
pub struct RPCGetTransactionRequestHandler {
55+
pub txid: Option<Txid>,
56+
}
57+
impl RPCGetTransactionRequestHandler {
58+
pub fn new() -> Self {
59+
Self { txid: None }
60+
}
61+
}
62+
63+
/// Decode the HTTP request
64+
impl HttpRequest for RPCGetTransactionRequestHandler {
65+
fn verb(&self) -> &'static str {
66+
"GET"
67+
}
68+
69+
fn path_regex(&self) -> Regex {
70+
Regex::new(r#"^/v3/transactions/(?P<txid>[0-9a-f]{64})$"#).unwrap()
71+
}
72+
73+
fn metrics_identifier(&self) -> &str {
74+
"/v3/transactions/:txid"
75+
}
76+
77+
/// Try to decode this request.
78+
/// There's nothing to load here, so just make sure the request is well-formed.
79+
fn try_parse_request(
80+
&mut self,
81+
preamble: &HttpRequestPreamble,
82+
captures: &Captures,
83+
query: Option<&str>,
84+
_body: &[u8],
85+
) -> Result<HttpRequestContents, Error> {
86+
if preamble.get_content_length() != 0 {
87+
return Err(Error::DecodeError(
88+
"Invalid Http request: expected 0-length body for GetTransaction".to_string(),
89+
));
90+
}
91+
92+
let txid = request::get_txid(captures, "txid")?;
93+
self.txid = Some(txid);
94+
95+
Ok(HttpRequestContents::new().query_string(query))
96+
}
97+
}
98+
99+
impl RPCRequestHandler for RPCGetTransactionRequestHandler {
100+
/// Reset internal state
101+
fn restart(&mut self) {
102+
self.txid = None;
103+
}
104+
105+
/// Make the response
106+
fn try_handle_request(
107+
&mut self,
108+
preamble: HttpRequestPreamble,
109+
_contents: HttpRequestContents,
110+
node: &mut StacksNodeState,
111+
) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
112+
if !node.txindex {
113+
return StacksHttpResponse::new_error(
114+
&preamble,
115+
&HttpNotImplemented::new("Transaction indexing is not enabled".into()),
116+
)
117+
.try_into_contents()
118+
.map_err(NetError::from);
119+
}
120+
121+
let txid = self
122+
.txid
123+
.take()
124+
.ok_or(NetError::SendError("`txid` no set".into()))?;
125+
126+
node.with_node_state(|_network, _sortdb, chainstate, _mempool, _rpc_args| {
127+
let index_block_hash_and_tx_hex_opt =
128+
match NakamotoChainState::get_index_block_hash_and_tx_hex_from_txid(
129+
chainstate.index_conn().conn(),
130+
txid,
131+
) {
132+
Ok(index_block_hash_and_tx_hex_opt) => index_block_hash_and_tx_hex_opt,
133+
Err(e) => {
134+
// nope -- error trying to check
135+
let msg = format!("Failed to load transaction: {:?}\n", &e);
136+
warn!("{}", &msg);
137+
return StacksHttpResponse::new_error(
138+
&preamble,
139+
&HttpServerError::new(msg),
140+
)
141+
.try_into_contents()
142+
.map_err(NetError::from);
143+
}
144+
};
145+
146+
match index_block_hash_and_tx_hex_opt {
147+
Some((index_block_hash, tx_hex)) => {
148+
let preamble = HttpResponsePreamble::ok_json(&preamble);
149+
let body = HttpResponseContents::try_from_json(&TransactionResponse {
150+
index_block_hash,
151+
tx: tx_hex,
152+
})?;
153+
return Ok((preamble, body));
154+
}
155+
None => {
156+
// txid not found
157+
return StacksHttpResponse::new_error(
158+
&preamble,
159+
&HttpNotFound::new(format!("No such transaction {:?}\n", &txid)),
160+
)
161+
.try_into_contents()
162+
.map_err(NetError::from);
163+
}
164+
}
165+
})
166+
}
167+
}
168+
169+
/// Decode the HTTP response
170+
impl HttpResponse for RPCGetTransactionRequestHandler {
171+
fn try_parse_response(
172+
&self,
173+
preamble: &HttpResponsePreamble,
174+
body: &[u8],
175+
) -> Result<HttpResponsePayload, Error> {
176+
let txinfo: TransactionResponse = parse_json(preamble, body)?;
177+
Ok(HttpResponsePayload::try_from_json(txinfo)?)
178+
}
179+
}
180+
181+
impl StacksHttpRequest {
182+
/// Make a new get-unconfirmed-tx request
183+
pub fn new_gettransaction(host: PeerHost, txid: Txid) -> StacksHttpRequest {
184+
StacksHttpRequest::new_for_peer(
185+
host,
186+
"GET".into(),
187+
format!("/v3/transactions/{}", &txid),
188+
HttpRequestContents::new(),
189+
)
190+
.expect("FATAL: failed to construct request from infallible data")
191+
}
192+
}
193+
194+
impl StacksHttpResponse {
195+
pub fn decode_gettransaction(self) -> Result<TransactionResponse, NetError> {
196+
let contents = self.get_http_payload_ok()?;
197+
let response_json: serde_json::Value = contents.try_into()?;
198+
let txinfo: TransactionResponse = serde_json::from_value(response_json)
199+
.map_err(|_e| Error::DecodeError("Failed to decode JSON".to_string()))?;
200+
Ok(txinfo)
201+
}
202+
}

stackslib/src/net/api/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ pub mod gettenure;
6969
pub mod gettenureinfo;
7070
pub mod gettenuretip;
7171
pub mod gettransaction_unconfirmed;
72+
pub mod gettransactions;
7273
pub mod liststackerdbreplicas;
7374
pub mod postblock;
7475
pub mod postblock_proposal;
@@ -134,6 +135,7 @@ impl StacksHttp {
134135
self.register_rpc_endpoint(
135136
gettransaction_unconfirmed::RPCGetTransactionUnconfirmedRequestHandler::new(),
136137
);
138+
self.register_rpc_endpoint(gettransactions::RPCGetTransactionRequestHandler::new());
137139
self.register_rpc_endpoint(getsigner::GetSignerRequestHandler::default());
138140
self.register_rpc_endpoint(
139141
liststackerdbreplicas::RPCListStackerDBReplicasRequestHandler::new(),

0 commit comments

Comments
 (0)