Skip to content

Commit 7c7f9b3

Browse files
committed
feat: support multi-miner integration test
* test: add nakamoto_integration test with multiple miners (this test uses the blind signer test signing channel) * fix: nakamoto miner communicates over the correct miner slot for their block election (rather than searching by pubkey) * fix: neon miner does not submit block commits if the next burn block is in nakamoto * feat: update `/v2/neighbors` to use qualified contract identifier's ToString and parse() for JSON serialization * perf: nakamoto miner caches the reward set for their tenure
1 parent e5cc717 commit 7c7f9b3

File tree

14 files changed

+776
-210
lines changed

14 files changed

+776
-210
lines changed

stackslib/src/chainstate/nakamoto/mod.rs

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,28 @@ impl MaturedMinerPaymentSchedules {
455455
}
456456
}
457457

458+
pub struct MinersDBInformation {
459+
signer_0_sortition: ConsensusHash,
460+
signer_1_sortition: ConsensusHash,
461+
latest_winner: u16,
462+
}
463+
464+
impl MinersDBInformation {
465+
pub fn get_signer_index(&self, sortition: &ConsensusHash) -> Option<u16> {
466+
if sortition == &self.signer_0_sortition {
467+
Some(0)
468+
} else if sortition == &self.signer_1_sortition {
469+
Some(1)
470+
} else {
471+
None
472+
}
473+
}
474+
475+
pub fn get_latest_winner_index(&self) -> u16 {
476+
self.latest_winner
477+
}
478+
}
479+
458480
/// Calculated matured miner rewards, from scheduled rewards
459481
#[derive(Debug, Clone)]
460482
pub struct MaturedMinerRewards {
@@ -4039,7 +4061,7 @@ impl NakamotoChainState {
40394061
pub fn make_miners_stackerdb_config(
40404062
sortdb: &SortitionDB,
40414063
tip: &BlockSnapshot,
4042-
) -> Result<StackerDBConfig, ChainstateError> {
4064+
) -> Result<(StackerDBConfig, MinersDBInformation), ChainstateError> {
40434065
let ih = sortdb.index_handle(&tip.sortition_id);
40444066
let last_winner_snapshot = ih.get_last_snapshot_with_sortition(tip.block_height)?;
40454067
let parent_winner_snapshot = ih.get_last_snapshot_with_sortition(
@@ -4051,13 +4073,13 @@ impl NakamotoChainState {
40514073
// go get their corresponding leader keys, but preserve the miner's relative position in
40524074
// the stackerdb signer list -- if a miner was in slot 0, then it should stay in slot 0
40534075
// after a sortition (and vice versa for 1)
4054-
let sns = if last_winner_snapshot.num_sortitions % 2 == 0 {
4055-
[last_winner_snapshot, parent_winner_snapshot]
4076+
let (latest_winner_idx, sns) = if last_winner_snapshot.num_sortitions % 2 == 0 {
4077+
(0, [last_winner_snapshot, parent_winner_snapshot])
40564078
} else {
4057-
[parent_winner_snapshot, last_winner_snapshot]
4079+
(1, [parent_winner_snapshot, last_winner_snapshot])
40584080
};
40594081

4060-
for sn in sns {
4082+
for sn in sns.iter() {
40614083
// find the commit
40624084
let Some(block_commit) =
40634085
ih.get_block_commit_by_txid(&sn.sortition_id, &sn.winning_block_txid)?
@@ -4088,6 +4110,12 @@ impl NakamotoChainState {
40884110
);
40894111
}
40904112

4113+
let miners_db_info = MinersDBInformation {
4114+
signer_0_sortition: sns[0].consensus_hash,
4115+
signer_1_sortition: sns[1].consensus_hash,
4116+
latest_winner: latest_winner_idx,
4117+
};
4118+
40914119
let signers = miner_key_hash160s
40924120
.into_iter()
40934121
.map(|hash160|
@@ -4101,14 +4129,17 @@ impl NakamotoChainState {
41014129
))
41024130
.collect();
41034131

4104-
Ok(StackerDBConfig {
4105-
chunk_size: MAX_PAYLOAD_LEN.into(),
4106-
signers,
4107-
write_freq: 0,
4108-
max_writes: u32::MAX, // no limit on number of writes
4109-
max_neighbors: 200, // TODO: const -- just has to be equal to or greater than the number of signers
4110-
hint_replicas: vec![], // TODO: is there a way to get the IP addresses of stackers' preferred nodes?
4111-
})
4132+
Ok((
4133+
StackerDBConfig {
4134+
chunk_size: MAX_PAYLOAD_LEN.into(),
4135+
signers,
4136+
write_freq: 0,
4137+
max_writes: u32::MAX, // no limit on number of writes
4138+
max_neighbors: 200, // TODO: const -- just has to be equal to or greater than the number of signers
4139+
hint_replicas: vec![], // TODO: is there a way to get the IP addresses of stackers' preferred nodes?
4140+
},
4141+
miners_db_info,
4142+
))
41124143
}
41134144

41144145
/// Get the slot range for the given miner's public key.
@@ -4119,33 +4150,29 @@ impl NakamotoChainState {
41194150
pub fn get_miner_slot(
41204151
sortdb: &SortitionDB,
41214152
tip: &BlockSnapshot,
4122-
miner_pubkey: &StacksPublicKey,
4153+
election_sortition: &ConsensusHash,
41234154
) -> Result<Option<Range<u32>>, ChainstateError> {
4124-
let miner_hash160 = Hash160::from_node_public_key(&miner_pubkey);
4125-
let stackerdb_config = Self::make_miners_stackerdb_config(sortdb, &tip)?;
4155+
let (stackerdb_config, miners_info) = Self::make_miners_stackerdb_config(sortdb, &tip)?;
41264156

41274157
// find out which slot we're in
4128-
let mut slot_index = 0;
4129-
let mut slot_id_result = None;
4130-
for (addr, slot_count) in stackerdb_config.signers.iter() {
4131-
if addr.bytes == miner_hash160 {
4132-
slot_id_result = Some(Range {
4133-
start: slot_index,
4134-
end: slot_index + slot_count,
4135-
});
4136-
break;
4137-
}
4138-
slot_index += slot_count;
4139-
}
4140-
4141-
let Some(slot_id_range) = slot_id_result else {
4142-
// miner key does not match any slot
4158+
let Some(signer_ix) = miners_info
4159+
.get_signer_index(election_sortition)
4160+
.map(usize::from)
4161+
else {
41434162
warn!("Miner is not in the miners StackerDB config";
4144-
"miner" => %miner_hash160,
41454163
"stackerdb_slots" => format!("{:?}", &stackerdb_config.signers));
4146-
41474164
return Ok(None);
41484165
};
4166+
let mut signer_ranges = stackerdb_config.signer_ranges();
4167+
if signer_ix >= signer_ranges.len() {
4168+
// should be unreachable, but always good to be careful
4169+
warn!("Miner is not in the miners StackerDB config";
4170+
"stackerdb_slots" => format!("{:?}", &stackerdb_config.signers));
4171+
4172+
return Ok(None);
4173+
}
4174+
let slot_id_range = signer_ranges.swap_remove(signer_ix);
4175+
41494176
Ok(Some(slot_id_range))
41504177
}
41514178

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2049,8 +2049,9 @@ fn test_make_miners_stackerdb_config() {
20492049

20502050
let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap();
20512051
// check the stackerdb config as of this chain tip
2052-
let stackerdb_config =
2053-
NakamotoChainState::make_miners_stackerdb_config(sort_db, &tip).unwrap();
2052+
let stackerdb_config = NakamotoChainState::make_miners_stackerdb_config(sort_db, &tip)
2053+
.unwrap()
2054+
.0;
20542055
eprintln!(
20552056
"stackerdb_config at i = {} (sorition? {}): {:?}",
20562057
&i, sortition, &stackerdb_config
@@ -2079,8 +2080,9 @@ fn test_make_miners_stackerdb_config() {
20792080
let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap();
20802081
let miner_privkey = &miner_keys[i];
20812082
let miner_pubkey = StacksPublicKey::from_private(miner_privkey);
2082-
let slot_id = NakamotoChainState::get_miner_slot(&sort_db, &tip, &miner_pubkey)
2083-
.expect("Failed to get miner slot");
2083+
let slot_id =
2084+
NakamotoChainState::get_miner_slot(&sort_db, &tip, &block.header.consensus_hash)
2085+
.expect("Failed to get miner slot");
20842086
if sortition {
20852087
let slot_id = slot_id.expect("No miner slot exists for this miner").start;
20862088
let slot_version = stackerdbs

stackslib/src/net/api/getneighbors.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,43 @@ pub struct RPCNeighbor {
5151
pub public_key_hash: Hash160,
5252
pub authenticated: bool,
5353
#[serde(skip_serializing_if = "Option::is_none")]
54+
#[serde(with = "serde_opt_vec_qci")]
5455
pub stackerdbs: Option<Vec<QualifiedContractIdentifier>>,
5556
}
5657

58+
/// Serialize and deserialize `Option<Vec<QualifiedContractIdentifier>>`
59+
/// using the `to_string()` and `parse()` implementations of `QualifiedContractIdentifier`.
60+
mod serde_opt_vec_qci {
61+
use clarity::vm::types::QualifiedContractIdentifier;
62+
use serde::{Deserialize, Serialize};
63+
64+
pub fn serialize<S: serde::Serializer>(
65+
opt: &Option<Vec<QualifiedContractIdentifier>>,
66+
serializer: S,
67+
) -> Result<S::Ok, S::Error> {
68+
let serialize_as: Option<Vec<_>> = opt
69+
.as_ref()
70+
.map(|vec_qci| vec_qci.iter().map(ToString::to_string).collect());
71+
serialize_as.serialize(serializer)
72+
}
73+
74+
pub fn deserialize<'de, D>(de: D) -> Result<Option<Vec<QualifiedContractIdentifier>>, D::Error>
75+
where
76+
D: serde::Deserializer<'de>,
77+
{
78+
let from_str: Option<Vec<String>> = Deserialize::deserialize(de)?;
79+
let Some(vec_str) = from_str else {
80+
return Ok(None);
81+
};
82+
let parse_opt: Result<Vec<QualifiedContractIdentifier>, _> = vec_str
83+
.into_iter()
84+
.map(|x| QualifiedContractIdentifier::parse(&x).map_err(serde::de::Error::custom))
85+
.collect();
86+
let out_vec = parse_opt?;
87+
Ok(Some(out_vec))
88+
}
89+
}
90+
5791
impl RPCNeighbor {
5892
pub fn from_neighbor_key_and_pubkh(
5993
nk: NeighborKey,

stackslib/src/net/rpc.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -554,12 +554,12 @@ impl ConversationHttp {
554554
)?;
555555

556556
info!("Handled StacksHTTPRequest";
557-
"verb" => %verb,
558-
"path" => %request_path,
559-
"processing_time_ms" => start_time.elapsed().as_millis(),
560-
"latency_ms" => latency,
561-
"conn_id" => self.conn_id,
562-
"peer_addr" => &self.peer_addr);
557+
"verb" => %verb,
558+
"path" => %request_path,
559+
"processing_time_ms" => start_time.elapsed().as_millis(),
560+
"latency_ms" => latency,
561+
"conn_id" => self.conn_id,
562+
"peer_addr" => &self.peer_addr);
563563

564564
if let Some(msg) = msg_opt {
565565
ret.push(msg);

stackslib/src/net/stackerdb/config.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -555,10 +555,17 @@ impl StackerDBConfig {
555555
reason,
556556
));
557557
} else if let Some(Err(e)) = res {
558-
warn!(
559-
"Could not use contract {} for StackerDB: {:?}",
560-
contract_id, &e
561-
);
558+
if contract_id.is_boot() {
559+
debug!(
560+
"Could not use contract {} for StackerDB: {:?}",
561+
contract_id, &e
562+
);
563+
} else {
564+
warn!(
565+
"Could not use contract {} for StackerDB: {:?}",
566+
contract_id, &e
567+
);
568+
}
562569
return Err(e);
563570
}
564571

stackslib/src/net/stackerdb/mod.rs

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ pub mod db;
119119
pub mod sync;
120120

121121
use std::collections::{HashMap, HashSet};
122+
use std::ops::Range;
122123

123124
use clarity::vm::types::QualifiedContractIdentifier;
124125
use libstackerdb::{SlotMetadata, STACKERDB_MAX_CHUNK_SIZE};
@@ -205,6 +206,22 @@ impl StackerDBConfig {
205206
pub fn num_slots(&self) -> u32 {
206207
self.signers.iter().fold(0, |acc, s| acc + s.1)
207208
}
209+
210+
/// What are the slot index ranges for each signer?
211+
/// Returns the ranges in the same ordering as `self.signers`
212+
pub fn signer_ranges(&self) -> Vec<Range<u32>> {
213+
let mut slot_index = 0;
214+
let mut result = Vec::with_capacity(self.signers.len());
215+
for (_, slot_count) in self.signers.iter() {
216+
let end = slot_index + *slot_count;
217+
result.push(Range {
218+
start: slot_index,
219+
end,
220+
});
221+
slot_index = end;
222+
}
223+
result
224+
}
208225
}
209226

210227
/// This is the set of replicated chunks in all stacker DBs that this node subscribes to.
@@ -280,14 +297,16 @@ impl StackerDBs {
280297
== boot_code_id(MINERS_NAME, chainstate.mainnet)
281298
{
282299
// .miners contract -- directly generate the config
283-
NakamotoChainState::make_miners_stackerdb_config(sortdb, &tip).unwrap_or_else(|e| {
284-
warn!(
285-
"Failed to generate .miners StackerDB config";
286-
"contract" => %stackerdb_contract_id,
287-
"err" => ?e,
288-
);
289-
StackerDBConfig::noop()
290-
})
300+
NakamotoChainState::make_miners_stackerdb_config(sortdb, &tip)
301+
.map(|(config, _)| config)
302+
.unwrap_or_else(|e| {
303+
warn!(
304+
"Failed to generate .miners StackerDB config";
305+
"contract" => %stackerdb_contract_id,
306+
"err" => ?e,
307+
);
308+
StackerDBConfig::noop()
309+
})
291310
} else {
292311
// attempt to load the config from the contract itself
293312
StackerDBConfig::from_smart_contract(
@@ -297,11 +316,20 @@ impl StackerDBs {
297316
num_neighbors,
298317
)
299318
.unwrap_or_else(|e| {
300-
warn!(
301-
"Failed to load StackerDB config";
302-
"contract" => %stackerdb_contract_id,
303-
"err" => ?e,
304-
);
319+
if matches!(e, net_error::NoSuchStackerDB(_)) && stackerdb_contract_id.is_boot()
320+
{
321+
debug!(
322+
"Failed to load StackerDB config";
323+
"contract" => %stackerdb_contract_id,
324+
"err" => ?e,
325+
);
326+
} else {
327+
warn!(
328+
"Failed to load StackerDB config";
329+
"contract" => %stackerdb_contract_id,
330+
"err" => ?e,
331+
);
332+
}
305333
StackerDBConfig::noop()
306334
})
307335
};

stackslib/src/net/stackerdb/sync.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,9 +1096,6 @@ impl<NC: NeighborComms> StackerDBSync<NC> {
10961096
// next-prioritized chunk
10971097
cur_priority = (cur_priority + 1) % self.chunk_push_priorities.len();
10981098
}
1099-
if pushed == 0 {
1100-
return Err(net_error::PeerNotConnected);
1101-
}
11021099
self.next_chunk_push_priority = cur_priority;
11031100
Ok(self
11041101
.chunk_push_priorities

0 commit comments

Comments
 (0)