Skip to content

Commit

Permalink
Merge branch 'refactor/wsts-cleanup' into feat/mock-signing
Browse files Browse the repository at this point in the history
  • Loading branch information
cylewitruk committed Feb 5, 2025
2 parents 2742370 + 68bf8f5 commit 978a83e
Show file tree
Hide file tree
Showing 29 changed files with 951 additions and 804 deletions.
1 change: 1 addition & 0 deletions .github/workflows/image-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
- blocklist-client

runs-on: ubuntu-latest
environment: "Push to Docker"
steps:
## Setup Docker for the builds
- name: Docker setup
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
- [sBTC Landing Page](https://sbtc.tech/)
- [sBTC Docs](https://docs.stacks.co/concepts/sbtc)

## Releases

See [`RELEASE.md`](./RELEASE.md).

## Design Docs

**All decisions are made and tracked via GitHub issues where they and their rationale can be verified publicly.** Due to sBTC's critical nature extensive research and planning has been done to ensure all funds remain secure on launch.
Expand Down Expand Up @@ -153,7 +157,7 @@ This GitHub Actions workflow, Cargo Vet, is designed to automate the vetting of
- **Failure**: Check the GitHub Actions logs for errors and annotations about unvetted dependencies.
Download the audit-suggestions.txt artifact from the "Artifacts" section of the GitHub Actions interface for a detailed report.

- **Addressing Unvetted Dependencies**: Use the suggestions in the audit-suggestions.txt file to update your dependency audit policies in the supply-chain.toml file.
- **Addressing Unvetted Dependencies**: Use the suggestions in the audit-suggestions.txt file to update your dependency audit policies in the supply-chain.toml file.

Running this command you are able to check the suggestions offline:

Expand Down
99 changes: 99 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# sBTC: Release Process

## Executive summary

This release process targets the following goals:

1. Ensure there exists a provable chain of trust connecting the source code to
the corresponding artifacts.
1. Ensure there exists a clear _separation of duties_ between those who _write_
the code and those who _release_ it (and announce its release).

It implements (1) by leveraging GitHub rulesets for branch/tag protection and
attestation for artifacts and (2) through the announcement process (described
below).

## [sBTC Core developers] Creating a new release

An sBTC release is a tagged commit on the `main` branch.

Any commit to `main` MUST require at least one review through a GitHub pull
request. Before merging the pull request, all tests MUST pass.

Tags MUST be named according to [semantic versioning][0].

[GitHub rulesets][1] ensure that only a subset of sBTC core developers can
create a `git tag`. Creating a tag SHOULD require 4-eyes (as of February 2025,
this is not yet possible).

Once a tag is created, a [GitHub deployment environment][2] will build and
publish any corresponding artifacts. The deployment environment MUST require a
review from a subset of sBTC core developers before executing. The use of
deployment environment ensures that all credentials that are required to publish artifacts
are gated behind the review process (e.g., Docker Hub credentials, until [OIDC
identities are supported][4]).

All artifacts MUST be [attested][3] so that their build provenance can be
established. This way, downstream users (e.g., sBTC signers) will be able to
cryptographically verify that an artifact (e.g., a Docker release) has been
built and published through GitHub actions.

All artifacts MUST be addressed through their cryptographic digest (e.g., `git
commit` or Docker image digest), in addition to their label (e.g., the `git
tag`).

To improve quality of life, the release notes MUST include breaking changes (if
any), upgrade migrations (if any), and a link to the relevant artifacts (e.g.,
Docker images).

## [sBTC Comms] Announcing a new release

After a new release has been created, the sBTC Comms team will inform the sBTC
signers and provide the appropriate update instructions.

All members of the sBTC Comms team MUST NOT be participating to sBTC development
(that is, they MUST NOT be part of the Core developers team). This ensures clear
separation of duties and, for instance, prevents a rogue core developer from
"convincing" the sBTC signers of deploying a tampered release.

At all times, there MUST be at least two members of the sBTC Comms team in any
communications channel including an sBTC signer (similarly to the 4-eyes process for releases).

## [sBTC Signers] Deploying a new release

Once sBTC Signers receive a release announcement from the sBTC Comms team, they
MUST:

1. Ensure the communication comes from a member of the sBTC Comms team.
1. Carefully read the corresponding upgrade instructions.
1. Verify the attestation of the attached artifacts.
1. Execute the upgrade.
1. Confirm the execution.

The `gh` executable can quickly [verify attestations][5]:

```bash
> gh attestation verify oci://index.docker.io/blockstack/sbtc:signer-0.0.9-rc6 -R stacks-network/sbtc
Loaded digest sha256:3bba86a5c2dfdbda61209dc728ab208406a909c8b5affba45da5bb4ccb27ad0d for oci://index.docker.io/blockstack/sbtc:signer-0.0.9-rc6
Loaded 1 attestation from GitHub API

The following policy criteria will be enforced:
- OIDC Issuer must match:................... https://token.actions.githubusercontent.com
- Source Repository Owner URI must match:... https://github.com/stacks-network
- Source Repository URI must match:......... https://github.com/stacks-network/sbtc
- Predicate type must match:................ https://slsa.dev/provenance/v1
- Subject Alternative Name must match regex: (?i)^https://github.com/stacks-network/sbtc/

✓ Verification succeeded!

sha256:3bba86a5c2dfdbda61209dc728ab208406a909c8b5affba45da5bb4ccb27ad0d was attested by:
REPO PREDICATE_TYPE WORKFLOW
stacks-network/sbtc https://slsa.dev/provenance/v1 .github/workflows/image-build.yaml@refs/tags/0.0.9-rc6
```

[0]: https://semver.org
[1]: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets
[2]: https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment
[3]: https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds
[4]: https://github.com/docker/roadmap/issues/314#issuecomment-2605945137
[5]: https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds#verifying-artifact-attestations-with-the-github-cli
9 changes: 4 additions & 5 deletions protobufs/stacks/signer/v1/messages.proto
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ message SignerMessage {

// A wsts message.
message WstsMessage {
// The transaction ID this message relates to, will be a dummy ID for DKG messages
bitcoin.BitcoinTxid txid = 1 [deprecated = true];
reserved 1;
// The wsts message
oneof inner {
// Tell signers to begin DKG by sending DKG public shares
Expand All @@ -63,14 +62,14 @@ message WstsMessage {
oneof id {
// If this WSTS message is related to a Bitcoin signing round, this field
// will be set to the related Bitcoin transaction ID.
bitcoin.BitcoinTxid id_bitcoin_txid = 12;
bitcoin.BitcoinTxid sweep = 12;
// If this WSTS message is related to a rotate-keys transaction, this field
// will be set to the _new_ aggregate public key being verified.
crypto.PublicKey id_rotate_key = 13;
crypto.PublicKey dkg_verification = 13;
// If this WSTS message is related to a DKG round, this field will be set
// to the 32-byte id determined based on the coordinator public key and
// block hash, set by the coordinator.
crypto.Uint256 id_dkg = 14;
crypto.Uint256 dkg = 14;
}
}

Expand Down
1 change: 0 additions & 1 deletion signer/src/bitcoin/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,6 @@ impl BitcoinPreSignRequest {
tx_fee: Amount::from_sat(tx.tx_fee),
reports,
chain_tip_height: btc_ctx.chain_tip_height,
// If the cap is None, then we assume that it is unlimited.
sbtc_limits: ctx.state().get_current_limits(),
};

Expand Down
7 changes: 2 additions & 5 deletions signer/src/bitcoin/zmq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,8 @@ pub struct BitcoinCoreMessageStream {
}

impl BitcoinCoreMessageStream {
/// Create a new one using the endpoint(s) in the config.
pub async fn new_from_endpoint<T>(endpoint: &str, _subscriptions: &[T]) -> Result<Self, Error>
where
T: AsRef<str>,
{
/// Create a new one using the given endpoint.
pub async fn new_from_endpoint(endpoint: &str) -> Result<Self, Error> {
let inner_stream = tokio::time::timeout(Duration::from_secs(10), async {
bitcoincore_zmq::subscribe_async_monitor(&[endpoint])
})
Expand Down
102 changes: 100 additions & 2 deletions signer/src/block_observer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! - Update signer set transactions
//! - Set aggregate key transactions
use std::collections::BTreeSet;
use std::future::Future;
use std::time::Duration;

Expand All @@ -29,6 +30,7 @@ use crate::context::SbtcLimits;
use crate::context::SignerEvent;
use crate::emily_client::EmilyInteract;
use crate::error::Error;
use crate::keys::PublicKey;
use crate::metrics::Metrics;
use crate::metrics::BITCOIN_BLOCKCHAIN;
use crate::stacks::api::GetNakamotoStartHeight as _;
Expand Down Expand Up @@ -157,8 +159,9 @@ where
tracing::warn!(%error, "could not process stacks blocks");
}

if let Err(error) = self.update_sbtc_limits().await {
tracing::warn!(%error, "could not update sBTC limits");
tracing::debug!("updating the signer state");
if let Err(error) = self.update_signer_state(block_hash).await {
tracing::warn!(%error, "could not update the signer state");
continue;
}

Expand Down Expand Up @@ -577,6 +580,101 @@ impl<C: Context, B> BlockObserver<C, B> {
}
Ok(())
}

/// Update the `SignerState` object with current signer set and
/// aggregate key data.
///
/// # Notes
///
/// The query used for fetching the cached information can take quite a
/// lot of some time to complete on mainnet. So this function updates
/// the signers state once so that the other event loops do not need to
/// execute them. The cached information is:
///
/// * The current signer set. It gets this information from the last
/// successful key-rotation contract call if it exists. If such a
/// contract call does not exist this function uses the latest DKG
/// shares, and if that doesn't exist it uses the bootstrap signing
/// set from the configuration.
/// * The current aggregate key. It gets this information from the last
/// successful key-rotation contract call if it exists, and from the
/// latest DKG shares if no such contract call can be found.
async fn set_signer_set_and_aggregate_key(&self, chain_tip: BlockHash) -> Result<(), Error> {
let (aggregate_key, public_keys) =
get_signer_set_and_aggregate_key(&self.context, chain_tip).await?;

let state = self.context.state();
if let Some(aggregate_key) = aggregate_key {
state.set_current_aggregate_key(aggregate_key);
}

state.update_current_signer_set(public_keys);
Ok(())
}

/// Update the `SignerState` object with data that is unlikely to
/// change until the arrival of the next bitcoin block.
///
/// # Notes
///
/// The function updates the following:
/// * sBTC limits from Emily.
/// * The current signer set.
/// * The current aggregate key.
async fn update_signer_state(&self, chain_tip: BlockHash) -> Result<(), Error> {
tracing::info!("loading sbtc limits from Emily");
self.update_sbtc_limits().await?;

tracing::info!("updating the signer state with the current signer set");
self.set_signer_set_and_aggregate_key(chain_tip).await
}
}

/// 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
/// None as the aggregate key if no DKG shares can be found, implying that
/// this signer has not participated in DKG.
#[tracing::instrument(skip_all)]
pub async fn get_signer_set_and_aggregate_key<C, B>(
context: &C,
chain_tip: B,
) -> Result<(Option<PublicKey>, BTreeSet<PublicKey>), Error>
where
C: Context,
B: Into<model::BitcoinBlockHash>,
{
let db = context.get_storage();
let chain_tip = chain_tip.into();

// 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 should have the same view of the latest
// aggregate key, 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(&chain_tip).await? {
Some(last_key) => {
let aggregate_key = last_key.aggregate_key;
let signer_set = last_key.signer_set.into_iter().collect();
Ok((Some(aggregate_key), signer_set))
}
None => match db.get_latest_encrypted_dkg_shares().await? {
Some(shares) => {
let signer_set = shares.signer_set_public_keys.into_iter().collect();
Ok((Some(shares.aggregate_key), signer_set))
}
None => Ok((None, context.config().signer.bootstrap_signing_set())),
},
}
}

#[cfg(test)]
Expand Down
Loading

0 comments on commit 978a83e

Please sign in to comment.