Skip to content

Commit 9e80c14

Browse files
authored
Merge pull request #5108 from jbencin/test/duplicate-signer
test(signer): Add test with duplicate signer private key
2 parents f1d90cc + bf860f4 commit 9e80c14

File tree

5 files changed

+160
-40
lines changed

5 files changed

+160
-40
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ jobs:
9999
- tests::signer::v0::reloads_signer_set_in
100100
- tests::signer::v0::signers_broadcast_signed_blocks
101101
- tests::signer::v0::min_gap_between_blocks
102+
- tests::signer::v0::duplicate_signers
102103
- tests::nakamoto_integrations::stack_stx_burn_op_integration_test
103104
- tests::nakamoto_integrations::check_block_heights
104105
- tests::nakamoto_integrations::clarity_burn_state

stacks-common/src/util/secp256k1.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use crate::util::hash::{hex_bytes, to_hex};
3838
// per-thread Secp256k1 context
3939
thread_local!(static _secp256k1: Secp256k1<secp256k1::All> = Secp256k1::new());
4040

41-
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
41+
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Hash)]
4242
pub struct Secp256k1PublicKey {
4343
// serde is broken for secp256k1, so do it ourselves
4444
#[serde(

testnet/stacks-node/src/tests/neon_integrations.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ pub mod test_observer {
574574
pub fn contains_burn_block_range(range: impl RangeBounds<u64>) -> Result<(), String> {
575575
// Get set of all burn block heights
576576
let burn_block_heights = get_blocks()
577-
.iter()
577+
.into_iter()
578578
.map(|x| x.get("burn_block_height").unwrap().as_u64().unwrap())
579579
.collect::<HashSet<_>>();
580580

testnet/stacks-node/src/tests/signer/mod.rs

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ impl<S: Signer<T> + Send + 'static, T: SignerEventTrait + 'static> SignerTest<Sp
125125
wait_on_signers,
126126
|_| {},
127127
|_| {},
128-
&[],
128+
None,
129+
None,
129130
)
130131
}
131132

@@ -138,12 +139,19 @@ impl<S: Signer<T> + Send + 'static, T: SignerEventTrait + 'static> SignerTest<Sp
138139
wait_on_signers: Option<Duration>,
139140
mut signer_config_modifier: F,
140141
mut node_config_modifier: G,
141-
btc_miner_pubkeys: &[Secp256k1PublicKey],
142+
btc_miner_pubkeys: Option<Vec<Secp256k1PublicKey>>,
143+
signer_stacks_private_keys: Option<Vec<StacksPrivateKey>>,
142144
) -> Self {
143145
// Generate Signer Data
144-
let signer_stacks_private_keys = (0..num_signers)
145-
.map(|_| StacksPrivateKey::new())
146-
.collect::<Vec<StacksPrivateKey>>();
146+
let signer_stacks_private_keys = signer_stacks_private_keys
147+
.inspect(|keys| {
148+
assert_eq!(
149+
keys.len(),
150+
num_signers,
151+
"Number of private keys does not match number of signers"
152+
)
153+
})
154+
.unwrap_or_else(|| (0..num_signers).map(|_| StacksPrivateKey::new()).collect());
147155

148156
let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None);
149157

@@ -159,11 +167,8 @@ impl<S: Signer<T> + Send + 'static, T: SignerEventTrait + 'static> SignerTest<Sp
159167
// That's the kind of thing an idiot would have on his luggage!
160168
let password = "12345";
161169
naka_conf.connection_options.auth_token = Some(password.to_string());
162-
if let Some(wait_on_signers) = wait_on_signers {
163-
naka_conf.miner.wait_on_signers = wait_on_signers;
164-
} else {
165-
naka_conf.miner.wait_on_signers = Duration::from_secs(10);
166-
}
170+
naka_conf.miner.wait_on_signers =
171+
wait_on_signers.unwrap_or_else(|| Duration::from_secs(10));
167172
let run_stamp = rand::random();
168173

169174
// Setup the signer and coordinator configurations
@@ -195,19 +200,20 @@ impl<S: Signer<T> + Send + 'static, T: SignerEventTrait + 'static> SignerTest<Sp
195200
.collect();
196201

197202
// Setup the nodes and deploy the contract to it
198-
let btc_miner_pubkeys = if btc_miner_pubkeys.is_empty() {
199-
let pk = Secp256k1PublicKey::from_hex(
200-
naka_conf
201-
.burnchain
202-
.local_mining_public_key
203-
.as_ref()
204-
.unwrap(),
205-
)
206-
.unwrap();
207-
vec![pk]
208-
} else {
209-
btc_miner_pubkeys.to_vec()
210-
};
203+
let btc_miner_pubkeys = btc_miner_pubkeys
204+
.filter(|keys| !keys.is_empty())
205+
.unwrap_or_else(|| {
206+
let pk = Secp256k1PublicKey::from_hex(
207+
naka_conf
208+
.burnchain
209+
.local_mining_public_key
210+
.as_ref()
211+
.unwrap(),
212+
)
213+
.unwrap();
214+
vec![pk]
215+
});
216+
211217
let node = setup_stx_btc_node(
212218
naka_conf,
213219
&signer_stacks_private_keys,
@@ -236,10 +242,10 @@ impl<S: Signer<T> + Send + 'static, T: SignerEventTrait + 'static> SignerTest<Sp
236242
continue;
237243
}
238244
let port = 3000 + signer_ix;
239-
let endpoint = format!("http://localhost:{}", port);
245+
let endpoint = format!("http://localhost:{port}");
240246
let path = format!("{endpoint}/status");
241247

242-
debug!("Issue status request to {}", &path);
248+
debug!("Issue status request to {path}");
243249
let client = reqwest::blocking::Client::new();
244250
let response = client
245251
.get(path)

testnet/stacks-node/src/tests/signer/v0.rs

Lines changed: 126 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ impl SignerTest<SpawnedSigner> {
321321
// Verify that the signers signed the proposed block
322322
let mut signer_index = 0;
323323
let mut signature_index = 0;
324+
let mut signing_keys = HashSet::new();
324325
let validated = loop {
325326
// Since we've already checked `signature.len()`, this means we've
326327
// validated all the signatures in this loop
@@ -331,6 +332,9 @@ impl SignerTest<SpawnedSigner> {
331332
error!("Failed to validate the mined nakamoto block: ran out of signers to try to validate signatures");
332333
break false;
333334
};
335+
if !signing_keys.insert(signer.signing_key) {
336+
panic!("Duplicate signing key detected: {:?}", signer.signing_key);
337+
}
334338
let stacks_public_key = Secp256k1PublicKey::from_slice(signer.signing_key.as_slice())
335339
.expect("Failed to convert signing key to StacksPublicKey");
336340
let valid = stacks_public_key
@@ -488,11 +492,7 @@ fn block_proposal_rejection() {
488492
while !found_signer_signature_hash_1 && !found_signer_signature_hash_2 {
489493
std::thread::sleep(Duration::from_secs(1));
490494
let chunks = test_observer::get_stackerdb_chunks();
491-
for chunk in chunks
492-
.into_iter()
493-
.map(|chunk| chunk.modified_slots)
494-
.flatten()
495-
{
495+
for chunk in chunks.into_iter().flat_map(|chunk| chunk.modified_slots) {
496496
let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
497497
else {
498498
continue;
@@ -796,7 +796,8 @@ fn reloads_signer_set_in() {
796796
Some(Duration::from_secs(15)),
797797
|_config| {},
798798
|_| {},
799-
&[],
799+
None,
800+
None,
800801
);
801802

802803
setup_epoch_3_reward_set(
@@ -925,7 +926,8 @@ fn forked_tenure_testing(
925926
config.broadcast_signed_blocks = false;
926927
},
927928
|_| {},
928-
&[],
929+
None,
930+
None,
929931
);
930932
let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
931933

@@ -1429,7 +1431,8 @@ fn multiple_miners() {
14291431
false
14301432
})
14311433
},
1432-
&[btc_miner_1_pk.clone(), btc_miner_2_pk.clone()],
1434+
Some(vec![btc_miner_1_pk.clone(), btc_miner_2_pk.clone()]),
1435+
None,
14331436
);
14341437
let conf = signer_test.running_nodes.conf.clone();
14351438
let mut conf_node_2 = conf.clone();
@@ -1694,7 +1697,8 @@ fn miner_forking() {
16941697
false
16951698
})
16961699
},
1697-
&[btc_miner_1_pk.clone(), btc_miner_2_pk.clone()],
1700+
Some(vec![btc_miner_1_pk.clone(), btc_miner_2_pk.clone()]),
1701+
None,
16981702
);
16991703
let conf = signer_test.running_nodes.conf.clone();
17001704
let mut conf_node_2 = conf.clone();
@@ -2274,7 +2278,8 @@ fn empty_sortition() {
22742278
config.block_proposal_timeout = block_proposal_timeout;
22752279
},
22762280
|_| {},
2277-
&[],
2281+
None,
2282+
None,
22782283
);
22792284
let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
22802285
let short_timeout = Duration::from_secs(20);
@@ -2455,7 +2460,8 @@ fn mock_sign_epoch_25() {
24552460
}
24562461
}
24572462
},
2458-
&[],
2463+
None,
2464+
None,
24592465
);
24602466

24612467
let epochs = signer_test
@@ -2659,7 +2665,8 @@ fn signer_set_rollover() {
26592665
}
26602666
naka_conf.node.rpc_bind = rpc_bind.clone();
26612667
},
2662-
&[],
2668+
None,
2669+
None,
26632670
);
26642671
assert_eq!(
26652672
new_spawned_signers[0].config.node_host,
@@ -2873,7 +2880,8 @@ fn min_gap_between_blocks() {
28732880
|config| {
28742881
config.miner.min_time_between_blocks_ms = time_between_blocks_ms;
28752882
},
2876-
&[],
2883+
None,
2884+
None,
28772885
);
28782886

28792887
let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
@@ -2945,3 +2953,108 @@ fn min_gap_between_blocks() {
29452953

29462954
signer_test.shutdown();
29472955
}
2956+
2957+
#[test]
2958+
#[ignore]
2959+
/// Test scenario where there are duplicate signers with the same private key
2960+
/// First submitted signature should take precedence
2961+
fn duplicate_signers() {
2962+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
2963+
return;
2964+
}
2965+
2966+
tracing_subscriber::registry()
2967+
.with(fmt::layer())
2968+
.with(EnvFilter::from_default_env())
2969+
.init();
2970+
2971+
// Disable p2p broadcast of the nakamoto blocks, so that we rely
2972+
// on the signer's using StackerDB to get pushed blocks
2973+
*nakamoto_node::miner::TEST_SKIP_P2P_BROADCAST
2974+
.lock()
2975+
.unwrap() = Some(true);
2976+
2977+
info!("------------------------- Test Setup -------------------------");
2978+
let num_signers = 5;
2979+
let mut signer_stacks_private_keys = (0..num_signers)
2980+
.map(|_| StacksPrivateKey::new())
2981+
.collect::<Vec<_>>();
2982+
2983+
// First two signers have same private key
2984+
signer_stacks_private_keys[1] = signer_stacks_private_keys[0];
2985+
let unique_signers = num_signers - 1;
2986+
let duplicate_pubkey = Secp256k1PublicKey::from_private(&signer_stacks_private_keys[0]);
2987+
let duplicate_pubkey_from_copy =
2988+
Secp256k1PublicKey::from_private(&signer_stacks_private_keys[1]);
2989+
assert_eq!(
2990+
duplicate_pubkey, duplicate_pubkey_from_copy,
2991+
"Recovered pubkeys don't match"
2992+
);
2993+
2994+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
2995+
num_signers,
2996+
vec![],
2997+
None,
2998+
|_| {},
2999+
|_| {},
3000+
None,
3001+
Some(signer_stacks_private_keys),
3002+
);
3003+
3004+
signer_test.boot_to_epoch_3();
3005+
let timeout = Duration::from_secs(30);
3006+
3007+
info!("------------------------- Try mining one block -------------------------");
3008+
3009+
signer_test.mine_and_verify_confirmed_naka_block(timeout, num_signers);
3010+
3011+
info!("------------------------- Read all `BlockResponse::Accepted` messages -------------------------");
3012+
3013+
let mut signer_accepted_responses = vec![];
3014+
let start_polling = Instant::now();
3015+
while start_polling.elapsed() <= timeout {
3016+
std::thread::sleep(Duration::from_secs(1));
3017+
let messages = test_observer::get_stackerdb_chunks()
3018+
.into_iter()
3019+
.flat_map(|chunk| chunk.modified_slots)
3020+
.filter_map(|chunk| {
3021+
SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()).ok()
3022+
})
3023+
.filter_map(|message| match message {
3024+
SignerMessage::BlockResponse(BlockResponse::Accepted(m)) => {
3025+
info!("Message(accepted): {message:?}");
3026+
Some(m)
3027+
}
3028+
_ => {
3029+
debug!("Message(ignored): {message:?}");
3030+
None
3031+
}
3032+
});
3033+
signer_accepted_responses.extend(messages);
3034+
}
3035+
3036+
info!("------------------------- Assert there are {unique_signers} unique signatures and recovered pubkeys -------------------------");
3037+
3038+
// Pick a message hash
3039+
let (selected_sighash, _) = signer_accepted_responses
3040+
.iter()
3041+
.min_by_key(|(sighash, _)| *sighash)
3042+
.copied()
3043+
.expect("No `BlockResponse::Accepted` messages recieved");
3044+
3045+
// Filter only resonses for selected block and collect unique pubkeys and signatures
3046+
let (pubkeys, signatures): (HashSet<_>, HashSet<_>) = signer_accepted_responses
3047+
.into_iter()
3048+
.filter(|(hash, _)| *hash == selected_sighash)
3049+
.map(|(msg, sig)| {
3050+
let pubkey = Secp256k1PublicKey::recover_to_pubkey(msg.bits(), &sig)
3051+
.expect("Failed to recover pubkey");
3052+
(pubkey, sig)
3053+
})
3054+
.unzip();
3055+
3056+
assert_eq!(pubkeys.len(), unique_signers);
3057+
assert_eq!(signatures.len(), unique_signers);
3058+
3059+
signer_test.shutdown();
3060+
}

0 commit comments

Comments
 (0)