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: use bootstrap signing set when missing rotate keys tx #655

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion emily/handler/src/api/handlers/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use crate::api::models::common::Status;
use crate::api::models::deposit::responses::{
GetDepositsForTransactionResponse, UpdateDepositsResponse,
};
use stacks_common::codec::StacksMessageCodec as _;
use crate::database::entries::StatusEntry;
use stacks_common::codec::StacksMessageCodec as _;
use warp::reply::{json, with_status, Reply};

use bitcoin::ScriptBuf;
Expand Down
1 change: 1 addition & 0 deletions signer/migrations/0003__create_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ 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,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL
);

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

Expand Down
30 changes: 22 additions & 8 deletions signer/src/storage/in_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ use bitcoin::OutPoint;
use blockstack_lib::types::chainstate::StacksBlockId;
use futures::StreamExt as _;
use futures::TryStreamExt as _;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::Mutex;

use crate::bitcoin::utxo::SignerUtxo;
Expand Down Expand Up @@ -78,7 +80,7 @@ pub struct Store {
pub stacks_nakamoto_blocks: HashMap<model::StacksBlockHash, model::StacksBlock>,

/// Encrypted DKG shares
pub encrypted_dkg_shares: HashMap<PublicKey, model::EncryptedDkgShares>,
pub encrypted_dkg_shares: BTreeMap<PublicKey, (OffsetDateTime, model::EncryptedDkgShares)>,

/// Rotate keys transactions
pub rotate_keys_transactions: HashMap<model::StacksTxId, model::RotateKeysTransaction>,
Expand Down Expand Up @@ -430,7 +432,19 @@ impl super::DbRead for SharedStore {
.await
.encrypted_dkg_shares
.get(aggregate_key)
.cloned())
.map(|(_, shares)| shares.clone()))
}

async fn get_last_encrypted_dkg_shares(
&self,
) -> Result<Option<model::EncryptedDkgShares>, Error> {
Ok(self
.lock()
.await
.encrypted_dkg_shares
.values()
.max_by_key(|(time, _)| time)
.map(|(_, shares)| shares.clone()))
}

async fn get_last_key_rotation(
Expand Down Expand Up @@ -465,7 +479,7 @@ impl super::DbRead for SharedStore {
.await
.encrypted_dkg_shares
.values()
.map(|share| share.script_pubkey.to_bytes())
.map(|(_, share)| share.script_pubkey.to_bytes())
.collect())
}

Expand Down Expand Up @@ -619,7 +633,7 @@ impl super::DbRead for SharedStore {
.await
.encrypted_dkg_shares
.values()
.any(|share| &share.script_pubkey == script))
.any(|(_, share)| &share.script_pubkey == script))
}

async fn get_bitcoin_tx(
Expand Down Expand Up @@ -853,10 +867,10 @@ impl super::DbWrite for SharedStore {
&self,
shares: &model::EncryptedDkgShares,
) -> Result<(), Error> {
self.lock()
.await
.encrypted_dkg_shares
.insert(shares.aggregate_key, shares.clone());
self.lock().await.encrypted_dkg_shares.insert(
shares.aggregate_key,
(time::OffsetDateTime::now_utc(), shares.clone()),
);

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

/// Return the most recent DKG shares, and return None if the table is
/// empty.
fn get_last_encrypted_dkg_shares(
&self,
) -> 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
5 changes: 5 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,8 @@ 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>,
}

/// Persisted public DKG shares from other signers
Expand Down
27 changes: 26 additions & 1 deletion signer/src/storage/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ impl super::DbRead for PgStore {
, script_pubkey
, encrypted_private_shares
, public_shares
, signer_set_public_keys
FROM sbtc_signer.dkg_shares
WHERE aggregate_key = $1;
"#,
Expand All @@ -981,6 +982,28 @@ impl super::DbRead for PgStore {
.map_err(Error::SqlxQuery)
}

async fn get_last_encrypted_dkg_shares(
&self,
) -> Result<Option<model::EncryptedDkgShares>, Error> {
sqlx::query_as::<_, model::EncryptedDkgShares>(
r#"
SELECT
aggregate_key
, tweaked_aggregate_key
, script_pubkey
, encrypted_private_shares
, public_shares
, signer_set_public_keys
FROM sbtc_signer.dkg_shares
ORDER BY created_at DESC
LIMIT 1;
"#,
)
.fetch_optional(&self.0)
.await
.map_err(Error::SqlxQuery)
}

/// Find the last key rotation by iterating backwards from the stacks
/// chain tip scanning all transactions until we encounter a key
/// rotation transactions.
Expand Down Expand Up @@ -1748,15 +1771,17 @@ impl super::DbWrite for PgStore {
, encrypted_private_shares
, public_shares
, script_pubkey
, signer_set_public_keys
)
VALUES ($1, $2, $3, $4, $5)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT DO NOTHING"#,
)
.bind(shares.aggregate_key)
.bind(shares.tweaked_aggregate_key)
.bind(&shares.encrypted_private_shares)
.bind(&shares.public_shares)
.bind(&shares.script_pubkey)
.bind(&shares.signer_set_public_keys)
.execute(&self.0)
.await
.map_err(Error::SqlxQuery)?;
Expand Down
1 change: 1 addition & 0 deletions signer/src/testing/dummy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ pub fn encrypted_dkg_shares<R: rand::RngCore + rand::CryptoRng>(
public_shares,
tweaked_aggregate_key: group_key.signers_tweaked_pubkey().unwrap(),
script_pubkey: group_key.signers_script_pubkey().into(),
signer_set_public_keys: vec![fake::Faker.fake_with_rng(rng)],
}
}

Expand Down
7 changes: 6 additions & 1 deletion signer/src/testing/transaction_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,11 +402,13 @@ where
let signer_private_key = signer_info.first().unwrap().signer_private_key.to_bytes();
let dummy_aggregate_key = PublicKey::from_private_key(&PrivateKey::new(&mut rng));

let signer_set = signer_info.first().unwrap().signer_public_keys.clone();
store_dummy_dkg_shares(
&mut rng,
&signer_private_key,
&handle.context.get_storage_mut(),
dummy_aggregate_key,
signer_set,
)
.await;

Expand Down Expand Up @@ -848,12 +850,15 @@ async fn store_dummy_dkg_shares<R, S>(
signer_private_key: &[u8; 32],
storage: &S,
group_key: PublicKey,
signer_set: BTreeSet<PublicKey>,
) where
R: rand::CryptoRng + rand::RngCore,
S: storage::DbWrite,
{
let shares =
let mut shares =
testing::dummy::encrypted_dkg_shares(&fake::Faker, rng, signer_private_key, group_key);
shares.signer_set_public_keys = signer_set.into_iter().collect();

storage
.write_encrypted_dkg_shares(&shares)
.await
Expand Down
53 changes: 41 additions & 12 deletions signer/src/transaction_coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ where
.ok_or(Error::NoChainTip)?;

let (aggregate_key, signer_public_keys) = self
.get_signer_public_keys_and_aggregate_key(&bitcoin_chain_tip)
.get_signer_set_and_aggregate_key(&bitcoin_chain_tip)
.await?;

if self.is_coordinator(&bitcoin_chain_tip, &signer_public_keys)? {
Expand Down Expand Up @@ -649,21 +649,50 @@ where
})
}

/// Return the signing set that can make sBTC related contract calls
/// along with the current aggregate key to use for locking UTXOs on
/// bitcoin.
///
/// The aggregate key fetched here is the one confirmed on the
/// canonical Stacks blockchain as part of a `rotate-keys` contract
/// call. It will be the public key that is the result of a DKG run. If
/// there are no rotate-keys transactions on the canonical stacks
/// blockchain, then we fall back on the last known DKG shares row in
/// our database, and return an error if is not found, implying that
/// DKG has never been run.
#[tracing::instrument(skip(self))]
async fn get_signer_public_keys_and_aggregate_key(
&mut self,
pub async fn get_signer_set_and_aggregate_key(
&self,
bitcoin_chain_tip: &model::BitcoinBlockHash,
) -> Result<(PublicKey, BTreeSet<PublicKey>), Error> {
let last_key_rotation = self
.context
.get_storage()
.get_last_key_rotation(bitcoin_chain_tip)
.await?
.ok_or(Error::MissingKeyRotation)?;
let db = self.context.get_storage();

let aggregate_key = last_key_rotation.aggregate_key;
let signer_set = last_key_rotation.signer_set.into_iter().collect();
Ok((aggregate_key, signer_set))
// We are supposed to submit a rotate-keys transaction after
// running DKG, but that transaction may not have been submitted
// yet (if we have just run DKG) or it may not have been confirmed
// on the canonical Stacks blockchain.
//
// If the signers have already run DKG, then we know that all
// participating signers have completed it successfully (well, some
// may have failed suddenly at the end, and some may have dropped
// off during DKG, but yeah enough should have their DKG shares).
// So we can fall back on the stored DKG shares for getting the
// current aggregate key and associated signing set.
match db.get_last_key_rotation(bitcoin_chain_tip).await? {
Some(last_key) => {
let aggregate_key = last_key.aggregate_key;
let signer_set = last_key.signer_set.into_iter().collect();
Ok((aggregate_key, signer_set))
}
None => {
let shares = db
.get_last_encrypted_dkg_shares()
.await?
.ok_or(Error::MissingDkgShares)?;
let signer_set = shares.signer_set_public_keys.into_iter().collect();
Ok((shares.aggregate_key, signer_set))
}
}
}

fn pub_key(&self) -> PublicKey {
Expand Down
Loading
Loading