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: ensure signers go through the bitcoin validation code path #930

Merged
merged 13 commits into from
Nov 25, 2024
60 changes: 34 additions & 26 deletions signer/src/bitcoin/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::bitcoin::utxo::SignerBtcState;
use crate::context::Context;
use crate::error::Error;
use crate::keys::PublicKey;
use crate::message::BitcoinPreSignRequest;
use crate::message::OutPointMessage;
use crate::storage::model::BitcoinBlockHash;
use crate::storage::model::BitcoinTxId;
Expand Down Expand Up @@ -43,16 +44,13 @@ pub struct BitcoinTxContext {
/// The block height of the bitcoin chain tip identified by the
/// `chain_tip` field.
pub chain_tip_height: u64,
/// This contains each of the requests for the entire transaction
/// package. Each element in the vector corresponds to the requests
/// that will be included in a single bitcoin transaction.
pub request_packages: Vec<TxRequestIds>,
/// How many bitcoin blocks back from the chain tip the signer will
/// look for the signer UTXO.
pub context_window: u16,
/// This signer's public key.
pub signer_public_key: PublicKey,
/// The current aggregate key that was the output of DKG.
pub aggregate_key: PublicKey,
/// The state of the signers.
pub signer_state: SignerBtcState,
}

/// This type is a container for all deposits and withdrawals that are part
Expand Down Expand Up @@ -81,28 +79,23 @@ impl From<&Requests<'_>> for TxRequestIds {
}

/// Check that this does not contain duplicate deposits or withdrawals.
pub fn is_unique(packages: &[TxRequestIds]) -> bool {
pub fn is_unique(package: &[TxRequestIds]) -> bool {
let mut deposits_set = HashSet::new();
let mut withdrawals_set = HashSet::new();
packages.iter().all(|reqs| {
package.iter().all(|reqs| {
let deposits = reqs.deposits.iter().all(|out| deposits_set.insert(out));
let withdrawals = reqs.withdrawals.iter().all(|id| withdrawals_set.insert(id));
deposits && withdrawals
})
}

impl BitcoinTxContext {
impl BitcoinPreSignRequest {
Jiloc marked this conversation as resolved.
Show resolved Hide resolved
/// Validate the current bitcoin transaction.
pub async fn pre_validation<C>(&self, _ctx: &C) -> Result<(), Error>
where
C: Context + Send + Sync,
{
if !is_unique(&self.request_packages) {
pub fn pre_validation(&self) -> Result<(), Error> {
if !is_unique(&self.request_package) {
return Err(Error::DuplicateRequests);
}

// TODO: check that we have not received a different transaction
// package during this tenure.
Ok(())
}

Expand All @@ -111,16 +104,30 @@ impl BitcoinTxContext {
pub async fn construct_package_sighashes<C>(
&self,
ctx: &C,
btc_ctx: &BitcoinTxContext,
) -> Result<Vec<BitcoinTxValidationData>, Error>
where
C: Context + Send + Sync,
{
let mut signer_state = self.signer_state;
self.pre_validation()?;
let signer_utxo = ctx
.get_storage()
.get_signer_utxo(&btc_ctx.chain_tip, btc_ctx.context_window)
.await?
.ok_or(Error::MissingSignerUtxo)?;

let mut signer_state = SignerBtcState {
fee_rate: self.fee_rate,
utxo: signer_utxo,
public_key: bitcoin::XOnlyPublicKey::from(btc_ctx.aggregate_key),
last_fees: self.last_fees,
magic_bytes: [b'T', b'3'], //TODO(#472): Use the correct magic bytes.
};
let mut outputs = Vec::new();

for requests in self.request_packages.iter() {
for requests in self.request_package.iter() {
let (output, new_signer_state) = self
.construct_tx_sighashes(ctx, requests, signer_state)
.construct_tx_sighashes(ctx, btc_ctx, requests, signer_state)
.await?;
signer_state = new_signer_state;
outputs.push(output);
Expand All @@ -138,6 +145,7 @@ impl BitcoinTxContext {
async fn construct_tx_sighashes<C>(
&self,
ctx: &C,
btc_ctx: &BitcoinTxContext,
requests: &TxRequestIds,
signer_state: SignerBtcState,
) -> Result<(BitcoinTxValidationData, SignerBtcState), Error>
Expand All @@ -146,9 +154,9 @@ impl BitcoinTxContext {
{
let db = ctx.get_storage();

let signer_public_key = &self.signer_public_key;
let aggregate_key = &self.aggregate_key;
let chain_tip = &self.chain_tip;
let signer_public_key = &btc_ctx.signer_public_key;
let aggregate_key = &btc_ctx.aggregate_key;
let chain_tip = &btc_ctx.chain_tip;

let mut deposits = Vec::new();
let mut withdrawals = Vec::new();
Expand All @@ -160,7 +168,7 @@ impl BitcoinTxContext {
db.get_deposit_request_report(chain_tip, &txid, output_index, signer_public_key);

let Some(report) = report_future.await? else {
return Err(InputValidationResult::Unknown.into_error(self));
return Err(InputValidationResult::Unknown.into_error(btc_ctx));
};

let votes = db
Expand All @@ -174,7 +182,7 @@ impl BitcoinTxContext {
let report_future = db.get_withdrawal_request_report(chain_tip, id, signer_public_key);

let Some(report) = report_future.await? else {
return Err(WithdrawalValidationResult::Unknown.into_error(self));
return Err(WithdrawalValidationResult::Unknown.into_error(btc_ctx));
};

let votes = db
Expand Down Expand Up @@ -207,11 +215,11 @@ impl BitcoinTxContext {
let out = BitcoinTxValidationData {
signer_sighash: sighashes.signer_sighash(),
deposit_sighashes: sighashes.deposit_sighashes(),
chain_tip: self.chain_tip,
chain_tip: btc_ctx.chain_tip,
tx: tx.tx.clone(),
tx_fee: Amount::from_sat(tx.tx_fee),
reports,
chain_tip_height: self.chain_tip_height,
chain_tip_height: btc_ctx.chain_tip_height,
};

Ok((out, signer_state))
Expand Down
16 changes: 16 additions & 0 deletions signer/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::emily_client::EmilyClientError;
use crate::stacks::contracts::DepositValidationError;
use crate::stacks::contracts::RotateKeysValidationError;
use crate::stacks::contracts::WithdrawalAcceptValidationError;
use crate::storage::model::SigHash;

/// Top-level signer error
#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -69,6 +70,21 @@ pub enum Error {
#[error("bitcoin validation error: {0}")]
BitcoinValidation(#[from] Box<crate::bitcoin::validation::BitcoinValidationError>),

/// This can only be thrown when the number of bytes for a sighash or
/// not exactly equal to 32. This should never occur.
#[error("could not convert message in nonce request to sighash {0}")]
SigHashConversion(#[source] bitcoin::hashes::FromSliceError),

/// This happens when the tx-signer is validating the sighash and it is
/// known but has failed validation.
#[error("the given sighash is known and failed validation: {0}")]
InvalidSigHash(SigHash),

/// This happens when the tx-signer is validating the sighash and it
/// does not have a row for it in the database.
#[error("the given sighash is unknown: {0}")]
UnknownSigHash(SigHash),

/// This should never happen
#[error("observed a tenure identified by a StacksBlockId with with no blocks")]
EmptyStacksTenure,
Expand Down
10 changes: 6 additions & 4 deletions signer/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,11 @@ impl From<OutPointMessage> for OutPoint {
/// The transaction context needed by the signers to reconstruct the transaction.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct BitcoinPreSignRequest {
/// The set of sBTC requests with additional relevant
/// information for the transaction.
pub requests: Vec<TxRequestIds>,
/// The set of sBTC request identifiers. This contains each of the
/// requests for the entire transaction package. Each element in the
/// vector corresponds to the requests that will be included in a
/// single bitcoin transaction.
pub request_package: Vec<TxRequestIds>,
/// The current market fee rate in sat/vByte.
pub fee_rate: f64,
/// The total fee amount and the fee rate for the last transaction that
Expand Down Expand Up @@ -532,7 +534,7 @@ impl wsts::net::Signable for TxRequestIds {
impl wsts::net::Signable for BitcoinPreSignRequest {
fn hash(&self, hasher: &mut sha2::Sha256) {
hasher.update("SIGNER_NEW_BITCOIN_TX_CONTEXT");
for request in &self.requests {
for request in &self.request_package {
request.hash(hasher);
}
hasher.update(self.fee_rate.to_be_bytes());
Expand Down
2 changes: 1 addition & 1 deletion signer/src/transaction_coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ where
// Get the requests from the transaction package because they have been split into
// multiple transactions.
let sbtc_requests = BitcoinPreSignRequest {
requests: transaction_package
request_package: transaction_package
.iter()
.map(|tx| (&tx.requests).into())
.collect(),
Expand Down
Loading