Skip to content

Commit 84a0f9a

Browse files
authored
Merge pull request #5253 from stacks-network/fix/5249
Fix/5249
2 parents c87c0eb + 638b26a commit 84a0f9a

File tree

12 files changed

+1147
-83
lines changed

12 files changed

+1147
-83
lines changed

stackslib/src/chainstate/nakamoto/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2533,6 +2533,18 @@ impl NakamotoChainState {
25332533
Ok(result)
25342534
}
25352535

2536+
/// Load a consensus hash for a Nakamoto header
2537+
pub fn get_block_header_nakamoto_tenure_id(
2538+
chainstate_conn: &Connection,
2539+
index_block_hash: &StacksBlockId,
2540+
) -> Result<Option<ConsensusHash>, ChainstateError> {
2541+
let sql = "SELECT consensus_hash FROM nakamoto_block_headers WHERE index_block_hash = ?1";
2542+
let result = query_row_panic(chainstate_conn, sql, &[&index_block_hash], || {
2543+
"FATAL: multiple rows for the same block hash".to_string()
2544+
})?;
2545+
Ok(result)
2546+
}
2547+
25362548
/// Load an epoch2 header
25372549
pub fn get_block_header_epoch2(
25382550
chainstate_conn: &Connection,

stackslib/src/chainstate/nakamoto/staging_blocks.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ impl<'a> NakamotoStagingBlocksConnRef<'a> {
271271
.optional()?)
272272
}
273273

274-
/// Get the rowid of a Nakamoto block
274+
/// Get the rowid of a staging Nakamoto block
275275
pub fn get_nakamoto_block_rowid(
276276
&self,
277277
index_block_hash: &StacksBlockId,
@@ -282,6 +282,26 @@ impl<'a> NakamotoStagingBlocksConnRef<'a> {
282282
Ok(res)
283283
}
284284

285+
/// Get the tenure and parent block ID of a staging block.
286+
/// Used for downloads
287+
pub fn get_tenure_and_parent_block_id(
288+
&self,
289+
index_block_hash: &StacksBlockId,
290+
) -> Result<Option<(ConsensusHash, StacksBlockId)>, ChainstateError> {
291+
let sql = "SELECT consensus_hash,parent_block_id FROM nakamoto_staging_blocks WHERE index_block_hash = ?1";
292+
let args = params![index_block_hash];
293+
294+
let mut stmt = self.deref().prepare(sql)?;
295+
Ok(stmt
296+
.query_row(args, |row| {
297+
let ch: ConsensusHash = row.get(0)?;
298+
let parent_id: StacksBlockId = row.get(1)?;
299+
300+
Ok((ch, parent_id))
301+
})
302+
.optional()?)
303+
}
304+
285305
/// Get a Nakamoto block by index block hash, as well as its size.
286306
/// Verifies its integrity.
287307
/// Returns Ok(Some(block, size)) if the block was present

stackslib/src/main.rs

Lines changed: 194 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ use std::collections::{BTreeMap, HashMap, HashSet};
3636
use std::fs::File;
3737
use std::io::prelude::*;
3838
use std::io::BufReader;
39+
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
40+
use std::time::Duration;
3941
use std::{env, fs, io, process, thread};
4042

4143
use blockstack_lib::burnchains::bitcoin::{spv, BitcoinNetworkType};
@@ -62,10 +64,12 @@ use blockstack_lib::clarity::vm::ClarityVersion;
6264
use blockstack_lib::core::{MemPoolDB, *};
6365
use blockstack_lib::cost_estimates::metrics::UnitMetric;
6466
use blockstack_lib::cost_estimates::UnitEstimator;
67+
use blockstack_lib::net::api::getinfo::RPCPeerInfoData;
6568
use blockstack_lib::net::db::LocalPeer;
69+
use blockstack_lib::net::httpcore::{send_http_request, StacksHttpRequest};
6670
use blockstack_lib::net::p2p::PeerNetwork;
6771
use blockstack_lib::net::relay::Relayer;
68-
use blockstack_lib::net::StacksMessage;
72+
use blockstack_lib::net::{GetNakamotoInvData, HandshakeData, StacksMessage, StacksMessageType};
6973
use blockstack_lib::util_lib::db::sqlite_open;
7074
use blockstack_lib::util_lib::strings::UrlString;
7175
use blockstack_lib::{clarity_cli, cli};
@@ -76,7 +80,7 @@ use stacks_common::codec::{read_next, StacksMessageCodec};
7680
use stacks_common::types::chainstate::{
7781
BlockHeaderHash, BurnchainHeaderHash, StacksAddress, StacksBlockId,
7882
};
79-
use stacks_common::types::net::PeerAddress;
83+
use stacks_common::types::net::{PeerAddress, PeerHost};
8084
use stacks_common::types::sqlite::NO_PARAMS;
8185
use stacks_common::types::MempoolCollectionBehavior;
8286
use stacks_common::util::hash::{hex_bytes, to_hex, Hash160};
@@ -85,6 +89,164 @@ use stacks_common::util::secp256k1::{Secp256k1PrivateKey, Secp256k1PublicKey};
8589
use stacks_common::util::vrf::VRFProof;
8690
use stacks_common::util::{get_epoch_time_ms, sleep_ms};
8791

92+
struct P2PSession {
93+
pub local_peer: LocalPeer,
94+
peer_info: RPCPeerInfoData,
95+
burn_block_hash: BurnchainHeaderHash,
96+
stable_burn_block_hash: BurnchainHeaderHash,
97+
tcp_socket: TcpStream,
98+
seq: u32,
99+
}
100+
101+
impl P2PSession {
102+
/// Make a StacksMessage. Sign it and set a sequence number.
103+
fn make_peer_message(&mut self, payload: StacksMessageType) -> Result<StacksMessage, String> {
104+
let mut msg = StacksMessage::new(
105+
self.peer_info.peer_version,
106+
self.peer_info.network_id,
107+
self.peer_info.burn_block_height,
108+
&self.burn_block_hash,
109+
self.peer_info.stable_burn_block_height,
110+
&self.stable_burn_block_hash,
111+
payload,
112+
);
113+
114+
msg.sign(self.seq, &self.local_peer.private_key)
115+
.map_err(|e| format!("Failed to sign message {:?}: {:?}", &msg, &e))?;
116+
self.seq = self.seq.wrapping_add(1);
117+
118+
Ok(msg)
119+
}
120+
121+
/// Send a p2p message.
122+
/// Returns error text on failure.
123+
fn send_peer_message(&mut self, msg: StacksMessage) -> Result<(), String> {
124+
msg.consensus_serialize(&mut self.tcp_socket)
125+
.map_err(|e| format!("Failed to send message {:?}: {:?}", &msg, &e))
126+
}
127+
128+
/// Receive a p2p message.
129+
/// Returns error text on failure.
130+
fn recv_peer_message(&mut self) -> Result<StacksMessage, String> {
131+
let msg: StacksMessage = read_next(&mut self.tcp_socket)
132+
.map_err(|e| format!("Failed to receive message: {:?}", &e))?;
133+
Ok(msg)
134+
}
135+
136+
/// Begin a p2p session.
137+
/// Synthesizes a LocalPeer from the remote peer's responses to /v2/info and /v2/pox.
138+
/// Performs the initial handshake for you.
139+
///
140+
/// Returns the session handle on success.
141+
/// Returns error text on failure.
142+
pub fn begin(peer_addr: SocketAddr, data_port: u16) -> Result<Self, String> {
143+
let mut data_addr = peer_addr.clone();
144+
data_addr.set_port(data_port);
145+
146+
// get /v2/info
147+
let peer_info = send_http_request(
148+
&format!("{}", data_addr.ip()),
149+
data_addr.port(),
150+
StacksHttpRequest::new_getinfo(PeerHost::from(data_addr.clone()), None)
151+
.with_header("Connection".to_string(), "close".to_string()),
152+
Duration::from_secs(60),
153+
)
154+
.map_err(|e| format!("Failed to query /v2/info: {:?}", &e))?
155+
.decode_peer_info()
156+
.map_err(|e| format!("Failed to decode response from /v2/info: {:?}", &e))?;
157+
158+
// convert `pox_consensus` and `stable_pox_consensus` into their respective burn block
159+
// hashes
160+
let sort_info = send_http_request(
161+
&format!("{}", data_addr.ip()),
162+
data_addr.port(),
163+
StacksHttpRequest::new_get_sortition_consensus(
164+
PeerHost::from(data_addr.clone()),
165+
&peer_info.pox_consensus,
166+
)
167+
.with_header("Connection".to_string(), "close".to_string()),
168+
Duration::from_secs(60),
169+
)
170+
.map_err(|e| format!("Failed to query /v3/sortitions: {:?}", &e))?
171+
.decode_sortition_info()
172+
.map_err(|e| format!("Failed to decode response from /v3/sortitions: {:?}", &e))?
173+
.pop()
174+
.ok_or_else(|| format!("No sortition returned for {}", &peer_info.pox_consensus))?;
175+
176+
let stable_sort_info = send_http_request(
177+
&format!("{}", data_addr.ip()),
178+
data_addr.port(),
179+
StacksHttpRequest::new_get_sortition_consensus(
180+
PeerHost::from(data_addr.clone()),
181+
&peer_info.stable_pox_consensus,
182+
)
183+
.with_header("Connection".to_string(), "close".to_string()),
184+
Duration::from_secs(60),
185+
)
186+
.map_err(|e| format!("Failed to query stable /v3/sortitions: {:?}", &e))?
187+
.decode_sortition_info()
188+
.map_err(|e| {
189+
format!(
190+
"Failed to decode response from stable /v3/sortitions: {:?}",
191+
&e
192+
)
193+
})?
194+
.pop()
195+
.ok_or_else(|| {
196+
format!(
197+
"No sortition returned for {}",
198+
&peer_info.stable_pox_consensus
199+
)
200+
})?;
201+
202+
let burn_block_hash = sort_info.burn_block_hash;
203+
let stable_burn_block_hash = stable_sort_info.burn_block_hash;
204+
205+
let local_peer = LocalPeer::new(
206+
peer_info.network_id,
207+
peer_info.parent_network_id,
208+
PeerAddress::from_socketaddr(&peer_addr),
209+
peer_addr.port(),
210+
Some(StacksPrivateKey::new()),
211+
u64::MAX,
212+
UrlString::try_from(format!("http://127.0.0.1:{}", data_port).as_str()).unwrap(),
213+
vec![],
214+
);
215+
216+
let tcp_socket = TcpStream::connect(&peer_addr)
217+
.map_err(|e| format!("Failed to open {:?}: {:?}", &peer_addr, &e))?;
218+
219+
let mut session = Self {
220+
local_peer,
221+
peer_info,
222+
burn_block_hash,
223+
stable_burn_block_hash,
224+
tcp_socket,
225+
seq: 0,
226+
};
227+
228+
// perform the handshake
229+
let handshake_data =
230+
StacksMessageType::Handshake(HandshakeData::from_local_peer(&session.local_peer));
231+
let handshake = session.make_peer_message(handshake_data)?;
232+
session.send_peer_message(handshake)?;
233+
234+
let resp = session.recv_peer_message()?;
235+
match resp.payload {
236+
StacksMessageType::HandshakeAccept(..)
237+
| StacksMessageType::StackerDBHandshakeAccept(..) => {}
238+
x => {
239+
return Err(format!(
240+
"Peer returned unexpected message (expected HandshakeAccept variant): {:?}",
241+
&x
242+
));
243+
}
244+
}
245+
246+
Ok(session)
247+
}
248+
}
249+
88250
#[cfg_attr(test, mutants::skip)]
89251
fn main() {
90252
let mut argv: Vec<String> = env::args().collect();
@@ -974,6 +1136,36 @@ simulating a miner.
9741136
process::exit(1);
9751137
}
9761138

1139+
if argv[1] == "getnakamotoinv" {
1140+
if argv.len() < 5 {
1141+
eprintln!(
1142+
"Usage: {} getnakamotoinv HOST:PORT DATA_PORT CONSENSUS_HASH",
1143+
&argv[0]
1144+
);
1145+
process::exit(1);
1146+
}
1147+
1148+
let peer_addr: SocketAddr = argv[2].to_socket_addrs().unwrap().next().unwrap();
1149+
let data_port: u16 = argv[3].parse().unwrap();
1150+
let ch = ConsensusHash::from_hex(&argv[4]).unwrap();
1151+
1152+
let mut session = P2PSession::begin(peer_addr, data_port).unwrap();
1153+
1154+
// send getnakamotoinv
1155+
let get_nakamoto_inv =
1156+
StacksMessageType::GetNakamotoInv(GetNakamotoInvData { consensus_hash: ch });
1157+
1158+
let msg = session.make_peer_message(get_nakamoto_inv).unwrap();
1159+
session.send_peer_message(msg).unwrap();
1160+
let resp = session.recv_peer_message().unwrap();
1161+
1162+
let StacksMessageType::NakamotoInv(inv) = &resp.payload else {
1163+
panic!("Got spurious message: {:?}", &resp);
1164+
};
1165+
1166+
println!("{:?}", inv);
1167+
}
1168+
9771169
if argv[1] == "replay-chainstate" {
9781170
if argv.len() < 7 {
9791171
eprintln!("Usage: {} OLD_CHAINSTATE_PATH OLD_SORTITION_DB_PATH OLD_BURNCHAIN_DB_PATH NEW_CHAINSTATE_PATH NEW_BURNCHAIN_DB_PATH", &argv[0]);

stackslib/src/net/api/getblock_v3.rs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -181,20 +181,13 @@ impl RPCRequestHandler for RPCNakamotoBlockRequestHandler {
181181

182182
let stream_res =
183183
node.with_node_state(|_network, _sortdb, chainstate, _mempool, _rpc_args| {
184-
let Some(header) =
185-
NakamotoChainState::get_block_header_nakamoto(chainstate.db(), &block_id)?
184+
let Some((tenure_id, parent_block_id)) = chainstate
185+
.nakamoto_blocks_db()
186+
.get_tenure_and_parent_block_id(&block_id)?
186187
else {
187188
return Err(ChainError::NoSuchBlockError);
188189
};
189-
let Some(nakamoto_header) = header.anchored_header.as_stacks_nakamoto() else {
190-
return Err(ChainError::NoSuchBlockError);
191-
};
192-
NakamotoBlockStream::new(
193-
chainstate,
194-
block_id.clone(),
195-
nakamoto_header.consensus_hash.clone(),
196-
nakamoto_header.parent_block_id.clone(),
197-
)
190+
NakamotoBlockStream::new(chainstate, block_id.clone(), tenure_id, parent_block_id)
198191
});
199192

200193
// start loading up the block

stackslib/src/net/api/getsortition.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,34 @@ impl HttpResponse for GetSortitionHandler {
381381
Ok(HttpResponsePayload::try_from_json(sortition_info)?)
382382
}
383383
}
384+
385+
impl StacksHttpRequest {
386+
/// Make a new getsortition request to this endpoint
387+
pub fn new_get_sortition(
388+
host: PeerHost,
389+
sort_key: &str,
390+
sort_value: &str,
391+
) -> StacksHttpRequest {
392+
StacksHttpRequest::new_for_peer(
393+
host,
394+
"GET".into(),
395+
format!("{}/{}/{}", RPC_SORTITION_INFO_PATH, sort_key, sort_value),
396+
HttpRequestContents::new(),
397+
)
398+
.expect("FATAL: failed to construct request from infallible data")
399+
}
400+
401+
pub fn new_get_sortition_consensus(host: PeerHost, ch: &ConsensusHash) -> StacksHttpRequest {
402+
Self::new_get_sortition(host, "consensus", &ch.to_string())
403+
}
404+
}
405+
406+
impl StacksHttpResponse {
407+
pub fn decode_sortition_info(self) -> Result<Vec<SortitionInfo>, NetError> {
408+
let contents = self.get_http_payload_ok()?;
409+
let response_json: serde_json::Value = contents.try_into()?;
410+
let response: Vec<SortitionInfo> = serde_json::from_value(response_json)
411+
.map_err(|_e| Error::DecodeError(format!("Failed to decode JSON: {:?}", &_e)))?;
412+
Ok(response)
413+
}
414+
}

stackslib/src/net/chat.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1744,7 +1744,8 @@ impl ConversationP2P {
17441744
&tip,
17451745
sortdb,
17461746
chainstate,
1747-
&network.stacks_tip.block_id(),
1747+
&network.stacks_tip.consensus_hash,
1748+
&network.stacks_tip.block_hash,
17481749
reward_cycle,
17491750
)?;
17501751
let nakamoto_inv = NakamotoInvData::try_from(&bitvec_bools).map_err(|e| {

stackslib/src/net/download/nakamoto/tenure_downloader.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,20 @@ impl NakamotoTenureDownloader {
376376
let mut expected_block_id = block_cursor;
377377
let mut count = 0;
378378
for block in tenure_blocks.iter() {
379-
if &block.header.block_id() != expected_block_id {
379+
// must be from this tenure
380+
// This may not always be the case, since a remote peer could have processed a
381+
// different Stacks micro-fork. The consequence of erroring here (or below) is that we
382+
// disconnect from the peer that served this to us.
383+
if block.header.consensus_hash != self.tenure_id_consensus_hash {
380384
warn!("Unexpected Nakamoto block -- not part of tenure";
385+
"block.header.consensus_hash" => %block.header.consensus_hash,
386+
"self.tenure_id_consensus_hash" => %self.tenure_id_consensus_hash,
387+
"state" => %self.state);
388+
return Err(NetError::InvalidMessage);
389+
}
390+
391+
if &block.header.block_id() != expected_block_id {
392+
warn!("Unexpected Nakamoto block -- does not match cursor";
381393
"expected_block_id" => %expected_block_id,
382394
"block_id" => %block.header.block_id(),
383395
"state" => %self.state);

stackslib/src/net/httpcore.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,12 @@ impl StacksHttpRequest {
532532
self.preamble.add_header(hdr, value);
533533
}
534534

535+
/// Constructor to add headers
536+
pub fn with_header(mut self, hdr: String, value: String) -> Self {
537+
self.add_header(hdr, value);
538+
self
539+
}
540+
535541
/// Get a ref to all request headers
536542
pub fn get_headers(&self) -> &BTreeMap<String, String> {
537543
&self.preamble.headers

0 commit comments

Comments
 (0)