Skip to content

Commit 3143a79

Browse files
authored
feat: use the signer set in the registry for picking the coordinator (#1912)
## Description Closes #1911 ## Changes * Use the registry as the source of truth for the signer set Note that these changes only matter if there is a signer set change that is about to happen. ## Testing Information I tested this branch on the usual flow on devenv, and it worked fine. I did not do any devenv testing that puts this new logic to the test though. ## Checklist - [x] I have performed a self-review of my code
1 parent f0fb3ba commit 3143a79

File tree

4 files changed

+301
-14
lines changed

4 files changed

+301
-14
lines changed

signer/src/context/mod.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ mod signer_context;
55
mod signer_state;
66
mod termination;
77

8+
use std::collections::BTreeSet;
9+
810
use tokio::sync::broadcast::error::RecvError;
911
use tokio_stream::wrappers::ReceiverStream;
1012

@@ -13,6 +15,7 @@ use crate::bitcoin::BitcoinInteract;
1315
use crate::config::Settings;
1416
use crate::emily_client::EmilyInteract;
1517
use crate::error::Error;
18+
use crate::keys::PublicKey;
1619
use crate::stacks::api::StacksInteract;
1720
use crate::storage::DbRead;
1821
use crate::storage::DbWrite;
@@ -107,4 +110,23 @@ pub trait Context: Clone + Sync + Send {
107110
});
108111
ReceiverStream::new(receiver)
109112
}
113+
114+
/// Return the signer set that is used when determining who is the
115+
/// coordinator.
116+
///
117+
/// # Notes
118+
///
119+
/// The signer set configured as the bootstrap signing set is used for
120+
/// deciding who this signer can communicate with and for whether we
121+
/// need to run DKG. However, for choosing a coordinator, we prefer to
122+
/// rely on what is in the registry since all signers see that at the
123+
/// same time yielding a more stable and predictable coordinator
124+
/// selection process.
125+
fn coordinator_signer_set(&self) -> BTreeSet<PublicKey> {
126+
let default_signer_set = || self.config().signer.bootstrap_signing_set.clone();
127+
128+
self.state()
129+
.registry_signer_set_info()
130+
.map_or_else(default_signer_set, |info| info.signer_set)
131+
}
110132
}

signer/src/context/signer_state.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ impl SignerState {
6565
self.registry_signing_set_info
6666
.read()
6767
.expect("BUG: Failed to acquire read lock of signer set info")
68-
.as_ref()
69-
.cloned()
68+
.to_owned()
7069
}
7170

7271
/// Get the current bitcoin chain tip.

signer/src/transaction_coordinator.rs

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,7 +1731,12 @@ where
17311731
S: Stream<Item = Signed<SignerMessage>>,
17321732
Coordinator: WstsCoordinator,
17331733
{
1734-
let signer_set = self.context.config().signer.bootstrap_signing_set.clone();
1734+
// We only allow the coordinator to send us certain kinds of
1735+
// messages. So later, we need to check whether the sender is the
1736+
// coordinator, and for that we need to use the "coordinator signer
1737+
// set", which may differ from the set of signers sending us
1738+
// messages right now.
1739+
let signer_set = self.context.coordinator_signer_set();
17351740
tokio::pin!(signal_stream);
17361741

17371742
// Let's get the next message from the network or the
@@ -1860,15 +1865,15 @@ where
18601865
/// Determine if this signer is the signer set's coordinator for the
18611866
/// specified bitcoin block hash.
18621867
///
1863-
/// The coordinator is decided using the hash of the bitcoin chain tip.
1864-
/// We don't use the chain tip directly because it typically starts
1865-
/// with a lot of leading zeros.
1868+
/// The coordinator is decided using the hash of the bitcoin chain tip
1869+
/// and signer set info from the registry if present. We don't use the
1870+
/// chain tip directly because it typically starts with a lot of
1871+
/// leading zeros.
18661872
pub fn is_coordinator(&self, bitcoin_chain_tip: &model::BitcoinBlockHash) -> bool {
1867-
given_key_is_coordinator(
1868-
self.signer_public_key(),
1869-
bitcoin_chain_tip,
1870-
&self.context.config().signer.bootstrap_signing_set,
1871-
)
1873+
let signer_public_keys = self.context.coordinator_signer_set();
1874+
1875+
let signer_public_key = self.signer_public_key();
1876+
given_key_is_coordinator(signer_public_key, bitcoin_chain_tip, &signer_public_keys)
18721877
}
18731878

18741879
/// Constructs a new [`utxo::SignerBtcState`] based on the current market
@@ -2858,6 +2863,66 @@ mod tests {
28582863
.unwrap();
28592864
}
28602865

2866+
/// Check that is_coordinator uses the registry for figuring out who
2867+
/// the cooridnator is and falls back to the config if the registry has
2868+
/// no signer set info.
2869+
#[tokio::test]
2870+
async fn is_coordinator_uses_registry_for_coordinator() {
2871+
let mut rng = testing::get_rng();
2872+
let ctx = TestContext::builder()
2873+
.with_in_memory_storage()
2874+
.with_mocked_clients()
2875+
.modify_settings(|settings| {
2876+
settings.signer.bootstrap_signatures_required = 1;
2877+
settings.signer.bootstrap_signing_set =
2878+
std::iter::once(settings.signer.public_key()).collect();
2879+
})
2880+
.build();
2881+
2882+
let network = WanNetwork::default();
2883+
let net = network.connect(&ctx);
2884+
2885+
let ev = TxCoordinatorEventLoop {
2886+
network: net.spawn(),
2887+
context: ctx.clone(),
2888+
context_window: 10000,
2889+
private_key: ctx.config().signer.private_key,
2890+
signing_round_max_duration: Duration::from_secs(10),
2891+
bitcoin_presign_request_max_duration: Duration::from_secs(10),
2892+
dkg_max_duration: Duration::from_secs(10),
2893+
is_epoch3: true,
2894+
};
2895+
2896+
// We should always be the coordinator since the registry is unset
2897+
// and we're the only signer in the bootstrap signing set.
2898+
assert!(ev.context.state().registry_signer_set_info().is_none());
2899+
2900+
for _ in 0..100 {
2901+
let chain_tip1: BitcoinBlockRef = fake::Faker.fake_with_rng(&mut rng);
2902+
assert!(ev.is_coordinator(&chain_tip1.block_hash));
2903+
}
2904+
2905+
// Now let's set the signer set in the registry to some random
2906+
// signer set without our key. Now we should never be the
2907+
// coordinator.
2908+
let signer_set_info = crate::stacks::api::SignerSetInfo {
2909+
aggregate_key: Faker.fake_with_rng(&mut rng),
2910+
signer_set: std::iter::repeat_with(|| Faker.fake_with_rng(&mut rng))
2911+
.take(2)
2912+
.collect(),
2913+
signatures_required: 2,
2914+
};
2915+
2916+
ctx.state().update_registry_signer_set_info(signer_set_info);
2917+
2918+
// If we were part of the signing set, there is a 2^(-128) chance
2919+
// that this check would pass.
2920+
for _ in 0..128 {
2921+
let chain_tip1: BitcoinBlockRef = fake::Faker.fake_with_rng(&mut rng);
2922+
assert!(!ev.is_coordinator(&chain_tip1.block_hash));
2923+
}
2924+
}
2925+
28612926
#[tokio::test]
28622927
async fn should_get_signer_utxo_simple() {
28632928
test_environment().assert_get_signer_utxo_simple().await;

0 commit comments

Comments
 (0)