Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bootstrapping distributed key generation (stale) #632

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions signer/migrations/0003__create_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ CREATE TABLE sbtc_signer.dkg_shares (
encrypted_private_shares BYTEA NOT NULL,
public_shares BYTEA NOT NULL,
script_pubkey BYTEA NOT NULL,
signer_set_public_keys BYTEA[] NOT NULL,
signer_set_aggregate_key BYTEA NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL
);

Expand Down
2 changes: 2 additions & 0 deletions signer/src/block_observer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,8 @@ mod tests {
script_pubkey: signers_script_pubkey.clone(),
encrypted_private_shares: Vec::new(),
public_shares: Vec::new(),
signer_set_public_keys: vec![aggregate_key],
signer_set_aggregate_key: aggregate_key,
};
storage.write_encrypted_dkg_shares(&shares).await.unwrap();

Expand Down
17 changes: 15 additions & 2 deletions signer/src/config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ nakamoto_start_height = 31
# Format: "<hex-encoded-private-key>" (64 or 66 hex-characters)
# Required: true
# Environment: SIGNER_SIGNER__PRIVATE_KEY
private_key = "8183dc385a7a1fc8353b9e781ee0859a71e57abea478a5bca679334094f7adb5"
private_key = "41634762d89dfa09133a4a8e9c1378d0161d29cd0a9433b51f1e3d32947a73dc"

# Specifies which network to use when constructing and sending transactions
# on stacks and bitcoin. This cooresponds to the `chain` flag in the
Expand All @@ -124,14 +124,27 @@ network = "regtest"
# The address that deployed the sbtc smart contracts.
#
# Required: true
deployer = "SN2V7WTJ7BHR03MPHZ1C9A9ZR6NZGR4WM8HT4V67Y"
deployer = "SN3R84XZYA63QS28932XQF3G1J8R9PC3W76P9CSQS"

# The signer database endpoint (pgsql connection string)
#
# Required: true
# Environment: SIGNER_SIGNER__DB_ENDPOINT
db_endpoint = "postgresql://postgres:postgres@localhost:5432/signer"

# The public keys of known signers who are approved to be in the signer
# set.
# The signer database endpoint (pgsql connection string)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snuck in your copy-paste ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, 🙏🏽 thank you

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: leftover line

#
# Required: true
# Environment: SIGNER_SIGNER__PEER_PUBLIC_KEYS
bootstrap_signing_set = [
"035249137286c077ccee65ecc43e724b9b9e5a588e3d7f51e3b62f9624c2a49e46",
"031a4d9f4903da97498945a4e01a5023a1d53bc96ad670bfe03adf8a06c52e6380",
"02007311430123d4cad97f4f7e86e023b28143130a18099ecf094d36fef0f6135c",
]

bootstrap_signatures_required = 2
# !! ==============================================================================
# !! Stacks Event Observer Configuration
# !!
Expand Down
40 changes: 39 additions & 1 deletion signer/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use config::File;
use libp2p::Multiaddr;
use serde::Deserialize;
use stacks_common::types::chainstate::StacksAddress;
use std::collections::BTreeSet;
use std::path::Path;
use url::Url;

Expand All @@ -16,6 +17,8 @@ use crate::config::serialization::private_key_deserializer;
use crate::config::serialization::url_deserializer_single;
use crate::config::serialization::url_deserializer_vec;
use crate::keys::PrivateKey;
use crate::keys::PublicKey;
use crate::stacks::wallet::SignerWallet;

mod error;
mod serialization;
Expand Down Expand Up @@ -225,6 +228,12 @@ pub struct SignerConfig {
/// The postgres database endpoint
#[serde(deserialize_with = "url_deserializer_single")]
pub db_endpoint: Url,
/// The public keys of the signer sit during the bootstrapping phase of
/// the signers.
pub bootstrap_signing_set: Vec<PublicKey>,
/// The number of signatures required for the signers' bootstrapped
/// multi-sig wallet on Stacks.
pub bootstrap_signatures_required: u16,
}

impl Validatable for SignerConfig {
Expand All @@ -243,12 +252,38 @@ impl Validatable for SignerConfig {
SignerConfigError::UnsupportedDatabaseDriver(self.db_endpoint.scheme().to_string());
return Err(ConfigError::Message(err.to_string()));
}

// The requirement here is that the bootstrap wallet in the config
// is a valid wallet, and all of those checks are done by the
// `SignerWallet::load_boostrap_wallet` function. Some of these
// checks include checks for an empty `bootstrap_signing_set`, or a
// `bootstrap_signatures_required` that is to high, there are
// others.
if let Err(err) = SignerWallet::load_boostrap_wallet(self) {
return Err(ConfigError::Message(err.to_string()));
}

// db_endpoint note: we don't validate the host because we will never
// get here; the URL deserializer will fail if the host is empty.
Ok(())
}
}

impl SignerConfig {
/// Return the bootstrapped signing set from the config. This function
/// makes sure that the signing set includes the current signer.
pub fn bootstrap_signing_set(&self) -> BTreeSet<PublicKey> {
// We add in the current signer into the signing set from the
// config just in case it hasn't been included already.
let self_public_key = PublicKey::from_private_key(&self.private_key);
self.bootstrap_signing_set
.iter()
.copied()
.chain([self_public_key])
.collect()
}
}

/// Configuration for the Stacks event observer server (hosted within the signer).
#[derive(Debug, Clone, Deserialize)]
pub struct EventObserverConfig {
Expand Down Expand Up @@ -288,6 +323,7 @@ impl Settings {
.separator("__")
.list_separator(",")
.try_parsing(true)
.with_list_parse_key("signer.peer_public_keys")
.with_list_parse_key("signer.p2p.seeds")
.with_list_parse_key("signer.p2p.listen_on")
.with_list_parse_key("signer.p2p.public_endpoints")
Expand Down Expand Up @@ -393,7 +429,7 @@ mod tests {
assert_eq!(
settings.signer.private_key,
PrivateKey::from_str(
"8183dc385a7a1fc8353b9e781ee0859a71e57abea478a5bca679334094f7adb5"
"41634762d89dfa09133a4a8e9c1378d0161d29cd0a9433b51f1e3d32947a73dc"
)
.unwrap()
);
Expand All @@ -418,6 +454,8 @@ mod tests {
settings.signer.event_observer.bind,
"0.0.0.0:8801".parse::<SocketAddr>().unwrap()
);
assert!(!settings.signer.bootstrap_signing_set.is_empty());
assert_eq!(settings.signer.bootstrap_signatures_required, 2);
}

#[test]
Expand Down
7 changes: 7 additions & 0 deletions signer/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,3 +411,10 @@ impl From<std::convert::Infallible> for Error {
match value {}
}
}

impl Error {
cylewitruk marked this conversation as resolved.
Show resolved Hide resolved
/// Convert a coordinator error to an `error::Error`
pub fn wsts_coordinator(err: wsts::state_machine::coordinator::Error) -> Self {
Error::WstsCoordinator(Box::new(err))
}
}
2 changes: 2 additions & 0 deletions signer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,8 @@ async fn run_transaction_coordinator(ctx: impl Context) -> Result<(), Error> {
private_key: config.signer.private_key,
signing_round_max_duration: Duration::from_secs(10),
threshold: 2,
bitcoin_network: config.signer.network.into(),
dkg_max_duration: Duration::from_secs(10),
};

coord.run().await
Expand Down
2 changes: 1 addition & 1 deletion signer/src/stacks/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1089,7 +1089,7 @@ mod tests {
SecretKey::new(&mut rng),
];
let public_keys = secret_keys.map(|sk| sk.public_key(SECP256K1).into());
let wallet = SignerWallet::new(&public_keys, 2, NetworkKind::Testnet, 0).unwrap();
let wallet = SignerWallet::new(public_keys, 2, NetworkKind::Testnet, 0).unwrap();
let deployer = StacksAddress::burn_address(false);

let call = RotateKeysV1::new(&wallet, deployer);
Expand Down
64 changes: 43 additions & 21 deletions signer/src/stacks/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use secp256k1::ecdsa::RecoverableSignature;
use secp256k1::Message;

use crate::config::NetworkKind;
use crate::config::SignerConfig;
use crate::context::Context;
use crate::error::Error;
use crate::keys::PublicKey;
Expand Down Expand Up @@ -98,13 +99,18 @@ impl SignerWallet {
/// is highly unlikely by chance, but a Byzantine actor could trigger
/// it purposefully if we don't require a signer to prove that they
/// control the public key that they submit.
pub fn new(
public_keys: &[PublicKey],
pub fn new<I>(
public_keys: I,
signatures_required: u16,
network_kind: NetworkKind,
nonce: u64,
) -> Result<Self, Error> {
) -> Result<Self, Error>
where
I: IntoIterator<Item = PublicKey>,
{
// Check most error conditions
let public_keys: BTreeSet<PublicKey> = public_keys.into_iter().collect();

let num_keys = public_keys.len();
let invalid_threshold = num_keys < signatures_required as usize;
let invalid_num_keys = num_keys == 0 || num_keys > MAX_KEYS as usize;
Expand All @@ -116,7 +122,6 @@ impl SignerWallet {
));
}

let public_keys: BTreeSet<PublicKey> = public_keys.iter().copied().collect();
// Used for creating the combined stacks address
let pubkeys: Vec<Secp256k1PublicKey> =
public_keys.iter().map(Secp256k1PublicKey::from).collect();
Expand Down Expand Up @@ -146,7 +151,9 @@ impl SignerWallet {
})
}

/// Load the multi-sig wallet from storage.
/// Load the multi-sig wallet from the last rotate-keys-trasnaction
/// stored in the database. If it's not there, fall back to the
/// bootstrap multi-sig wallet in the signer's config.
///
/// The wallet that is loaded is the one that cooresponds to the signer
/// set defined in the last confirmed key rotation contract call.
Expand All @@ -156,15 +163,26 @@ impl SignerWallet {
{
// Get the key rotation transaction from the database. This maps to
// what the stacks network thinks the signers' address is.
let last_key_rotation = ctx
.get_storage()
.get_last_key_rotation(chain_tip)
.await?
.ok_or(Error::MissingKeyRotation)?;
let last_key_rotation = ctx.get_storage().get_last_key_rotation(chain_tip).await?;

let config = &ctx.config().signer;
let network_kind = config.network;

match last_key_rotation {
Some(keys) => {
let public_keys = keys.signer_set;
let signatures_required = keys.signatures_required;
SignerWallet::new(public_keys, signatures_required, network_kind, 0)
}
None => Self::load_boostrap_wallet(config),
}
}

let public_keys = last_key_rotation.signer_set.as_slice();
let signatures_required = last_key_rotation.signatures_required;
let network_kind = ctx.config().signer.network;
/// Load the bootstrap wallet implicitly defined in the signer config.
pub fn load_boostrap_wallet(config: &SignerConfig) -> Result<SignerWallet, Error> {
let network_kind = config.network;
let public_keys = config.bootstrap_signing_set();
let signatures_required = config.bootstrap_signatures_required;

SignerWallet::new(public_keys, signatures_required, network_kind, 0)
}
Expand Down Expand Up @@ -443,7 +461,7 @@ mod tests {
.collect();

let public_keys: Vec<_> = key_pairs.iter().map(|kp| kp.public_key().into()).collect();
let wallet = SignerWallet::new(&public_keys, signatures_required, network, 1).unwrap();
let wallet = SignerWallet::new(public_keys, signatures_required, network, 1).unwrap();

let mut tx_signer = MultisigTx::new_contract_call(TestContractCall, &wallet, TX_FEE);
let tx = tx_signer.tx();
Expand Down Expand Up @@ -490,7 +508,7 @@ mod tests {
.collect();

let public_keys: Vec<_> = key_pairs.iter().map(|kp| kp.public_key().into()).collect();
let wallet = SignerWallet::new(&public_keys, signatures_required, network, 1).unwrap();
let wallet = SignerWallet::new(public_keys, signatures_required, network, 1).unwrap();

let mut tx_signer = MultisigTx::new_contract_call(TestContractCall, &wallet, TX_FEE);

Expand Down Expand Up @@ -547,15 +565,14 @@ mod tests {
.collect();

let pks1 = public_keys.clone();
let wallet1 = SignerWallet::new(&pks1, 5, network, 0).unwrap();

// Although it's unlikely, it's possible for the shuffle to not
// shuffle anything, so we need to keep trying.
while pks1 == public_keys {
public_keys.shuffle(&mut OsRng);
}
let wallet1 = SignerWallet::new(pks1, 5, network, 0).unwrap();

let wallet2 = SignerWallet::new(&public_keys, 5, network, 0).unwrap();
let wallet2 = SignerWallet::new(public_keys, 5, network, 0).unwrap();

assert_eq!(wallet1.address(), wallet2.address())
}
Expand Down Expand Up @@ -600,7 +617,8 @@ mod tests {
.collect();
let signatures_required = 5;
let network = NetworkKind::Regtest;
let wallet1 = SignerWallet::new(&signer_keys, signatures_required, network, 0).unwrap();
let public_keys = signer_keys.clone();
let wallet1 = SignerWallet::new(public_keys, signatures_required, network, 0).unwrap();

// Let's store the key information about this wallet into the database
let rotate_keys = RotateKeysTransaction {
Expand All @@ -618,8 +636,12 @@ mod tests {
.unwrap();

// We haven't stored any RotateKeysTransactions into the database
// yet, so loading the wallet should fail.
assert!(SignerWallet::load(&ctx, &bitcoin_chain_tip).await.is_err());
// yet, so it will try to load the wallet from the context.
let ans = SignerWallet::load(&ctx, &bitcoin_chain_tip).await.unwrap();
let config = &ctx.config().signer;
let bootstrap_aggregate_key =
PublicKey::combine_keys(&config.bootstrap_signing_set()).unwrap();
assert_eq!(ans.aggregate_key, bootstrap_aggregate_key);

let tx = model::StacksTransaction {
txid: rotate_keys.txid,
Expand Down
13 changes: 13 additions & 0 deletions signer/src/storage/in_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,19 @@ impl super::DbRead for SharedStore {
.cloned())
}

async fn get_encrypted_dkg_shares_by_signing_set(
&self,
signer_set_aggregate_key: &PublicKey,
) -> Result<Option<model::EncryptedDkgShares>, Error> {
Ok(self
.lock()
.await
.encrypted_dkg_shares
.values()
.find(|shares| &shares.signer_set_aggregate_key == signer_set_aggregate_key)
.cloned())
}

async fn get_last_key_rotation(
&self,
chain_tip: &model::BitcoinBlockHash,
Expand Down
8 changes: 8 additions & 0 deletions signer/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ pub trait DbRead {
aggregate_key: &PublicKey,
) -> impl Future<Output = Result<Option<model::EncryptedDkgShares>, Error>> + Send;

/// Return the applicable DKG shares for the given aggregate key of the
/// signing set. If there are more than one such key, this function
/// returns the one that was stored most recently.
fn get_encrypted_dkg_shares_by_signing_set(
&self,
signer_set_aggregate_key: &PublicKey,
) -> impl Future<Output = Result<Option<model::EncryptedDkgShares>, Error>> + Send;

/// Return the latest rotate-keys transaction confirmed by the given `chain-tip`.
fn get_last_key_rotation(
&self,
Expand Down
8 changes: 8 additions & 0 deletions signer/src/storage/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,9 @@ pub struct SweptWithdrawalRequest {
}

/// Persisted DKG shares
///
/// This struct represents the output of a successful run of distributed
/// key generation (DKG) that was run by a set of signers.
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, sqlx::FromRow)]
#[cfg_attr(feature = "testing", derive(fake::Dummy))]
pub struct EncryptedDkgShares {
Expand All @@ -323,6 +326,11 @@ pub struct EncryptedDkgShares {
pub encrypted_private_shares: Bytes,
/// The public DKG shares
pub public_shares: Bytes,
/// The set of public keys that were a party to the DKG.
pub signer_set_public_keys: Vec<PublicKey>,
/// The aggregate key of the public keys that formed a party during the
/// DKG associated with this struct instance.
pub signer_set_aggregate_key: PublicKey,
}

/// Persisted public DKG shares from other signers
Expand Down
Loading
Loading