-
Notifications
You must be signed in to change notification settings - Fork 16
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: DKG verification pre-key-rotation #1301
base: feat/require-all-signatures-for-rotate-keys
Are you sure you want to change the base?
feat: DKG verification pre-key-rotation #1301
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, nice work. Have have some high level comments:
- Why add a new table? Why not just a simple
is_verified
boolean column that is nullable, whereNULL
means that verification has not been attempted, false means that verification has been attempted and failed and true means that it has been successfully verified. It's much simplier, it's intuitive (to me at least), and gives us the functionality that we need. - Why support revocation of the keys? Once they're verified they'll stay verified. I would think that verification is a one way street, it goes from unknown to either passing or failing verification. Than again, maybe this is a language thing.
- I would add column comments in the database instead of a description field, with
COMMENT ON COLUMN blah IS 'some comment'
. If we do not want to add column comments, then we can just use regular code comments. - Why not use the
bitcoin::Transaction::verify_with_flags
function inverify_signature
? It's much simpler and does the same thing. - I feel like the block hash and block height columns in the dkg_shares should be linked to the started at height, not the ended/verified at. In the future, we should support DKG ending on a block that is different from the block that it started at. If we do that, it's possible for signers to have ending blocks (so different verified at blocks). But, maybe this is really just something to do with naming rather than meaning?
/// construct a [`Transaction`] with a single input and output. | ||
pub fn new(signer_public_key: XOnlyPublicKey) -> Self { | ||
let utxo = SignerUtxo { | ||
outpoint: OutPoint::new(Txid::all_zeros(), 0), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine, but OutPoint::null()
is even simpler.
input: vec![utxo.as_tx_input(&DUMMY_SIGNATURE)], | ||
output: vec![TxOut { | ||
value: Amount::from_sat(Self::AMOUNT), | ||
script_pubkey: ScriptBuf::new_op_return([0x01, 0x02, 0x03]), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine, but ScriptBuf::new_op_return([])
is even simpler.
|
||
// Encode the transaction to bytes (needed by the bitcoinconsensus | ||
// library). | ||
let mut tx_bytes: Vec<u8> = Vec::new(); | ||
tx.consensus_encode(&mut tx_bytes) | ||
.map_err(Error::BitcoinIo)?; | ||
|
||
// Get the prevout for the signers' UTXO. | ||
let prevout = self.utxo.as_tx_output(); | ||
let prevout_script_bytes = prevout.script_pubkey.as_script().as_bytes(); | ||
|
||
// Create the bitcoinconsensus UTXO object. | ||
let prevout_utxo = bitcoinconsensus::Utxo { | ||
script_pubkey: prevout_script_bytes.as_ptr(), | ||
script_pubkey_len: prevout_script_bytes.len() as u32, | ||
value: Self::AMOUNT as i64, | ||
}; | ||
|
||
// We specify the flags to include all pre-taproot and taproot | ||
// verifications explicitly. | ||
// https://github.com/rust-bitcoin/rust-bitcoinconsensus/blob/master/src/lib.rs | ||
let flags = bitcoinconsensus::VERIFY_ALL_PRE_TAPROOT | bitcoinconsensus::VERIFY_TAPROOT; | ||
|
||
// Verify that the transaction updated with the provided signature can | ||
// successfully spend the signers' UTXO. Note that the amount is not | ||
// used in the verification process for taproot spends, only the | ||
// signature. | ||
bitcoinconsensus::verify_with_flags( | ||
prevout_script_bytes, | ||
Self::AMOUNT, | ||
&tx_bytes, | ||
Some(&[prevout_utxo]), | ||
0, | ||
flags, | ||
) | ||
.map_err(Error::bitcoin_consensus) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should simplify the above to
// We specify the flags to include all pre-taproot and taproot
// verifications explicitly.
// https://github.com/rust-bitcoin/rust-bitcoinconsensus/blob/d1a3554fbf031beb725883b932155f8316887769/src/lib.rs
let flags = bitcoinconsensus::VERIFY_ALL_PRE_TAPROOT | bitcoinconsensus::VERIFY_TAPROOT;
tx.verify_with_flags(|_| Some(self.utxo.as_tx_output()), flags)
.map_err(Error::BitcoinConsensus);
or something similar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out that this doesn't work, as our unit test does not pass with this change. After some digging, we need a later version of bitcoinconsensus
in order for the verification to work, and so we need the changes here.
/// Convert a bitcoin consensus error to an `error::Error` | ||
pub fn bitcoin_consensus(err: bitcoinconsensus::Error) -> Self { | ||
Error::BitcoinConsensus(err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this seems unnecessary, we can replace Error::bitcoin_consensus
with Error::BitcoinConsensus
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pretty sure i tried that and ended up here, but will try it again
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General approach LGTM, sending some preliminary comments, but I'm also +1 Daniel points that could simplify some scaffolding in here. I would also add (at query level?) that you can set the verify (being successful or failed) only to pending DKG entries.
let keypair = secp256k1::Keypair::from_secret_key(&secp, &secret_key); | ||
let schnorr_sig = secp.sign_schnorr(&message, &keypair); | ||
let taproot_sig = bitcoin::taproot::Signature { | ||
signature: schnorr_sig, | ||
sighash_type: TapSighashType::All, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also test with tap_tweak
here? To be sure this is not failing because of an untweaked key
/// The rotate-key frost verification signing round failed for the aggregate | ||
/// key. | ||
#[error("rotate-key frost verification signing failed for aggregate key: {0}")] | ||
DkgVerificationFailed(Box<PublicKeyXOnly>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how does this differ from DkgVerificationNotSuccessful
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was about to ask the same :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe I meant this as "The DKG verification is not successful (yet?)" (i.e. when validating a key-rotation contract call), whereas DkgVerificationFailed
is hard fail
@@ -703,6 +714,78 @@ where | |||
Ok(()) | |||
} | |||
|
|||
/// Performs verification of of the DKG process by running a FROST signing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Performs verification of of the DKG process by running a FROST signing | |
/// Performs verification of the DKG process by running a FROST signing |
/// [`UnsignedMockTransaction`] using the new aggregate key using the FROST | ||
/// coordinator, which requires 100% signing participation vs. FIRE which | ||
/// only uses {threshold} signers. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the last part is a repetition of the paragraph above
.inspect_err(|error| { | ||
tracing::warn!(%error, "🔐 failed to assert that all signers have signed with the new aggregate key; aborting"); | ||
})?; | ||
tracing::info!("🔐 all signers have signed with the new aggregate key; proceeding"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: should we verify here that the signature is valid as well? To avoid having the coordinator reporting DKG verification successful
and attempting to sign the rotate key (that will fail, as the signers would refuse)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah good idea
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WSTS
will only return OperationResult::SignTaproot
if the signature passes verification. But there's no harm in verifying it again, it only takes milliseconds.
signer/src/transaction_signer.rs
Outdated
Some(OperationResult::SignSchnorr(_)) => { | ||
tracing::info!("successfully completed pre-rotate-key validation signing round"); | ||
self.wsts_frost_results.put(id, true); | ||
Some(OperationResult::SignSchnorr(sig) | OperationResult::SignTaproot(sig)) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is SignSchnorr
still relevant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think I just left it since it's what's done in the coordinator, but we're not requesting a SignatureType::Schnorr
anymore so it can probably be removed without issue.
signer/src/transaction_signer.rs
Outdated
@@ -152,7 +154,7 @@ pub struct TxSignerEventLoop<Context, Network, Rng> { | |||
/// verification of the Stacks rotate-keys transaction. | |||
pub wsts_frost_state_machines: LruCache<StateMachineId, FrostCoordinator>, | |||
/// Results of the FROST verification rounds. | |||
pub wsts_frost_results: LruCache<StateMachineId, bool>, | |||
pub wsts_frost_mock_txs: LruCache<StateMachineId, UnsignedMockTransaction>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we merge this with the above (ie, LruCache<StateMachineId, (FrostCoordinator, UnsignedMockTransaction)>
, or wrapping both in a struct)? So that we are sure we always have both available, given that we have an lru and the state machine usage can be decoupled from the mock transaction we may end up in some strange edge case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I've been wanting to do this, just haven't gotten to it yet -- will try to get it done soon
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good modulo a few comments and questions.
Cargo.toml
Outdated
@@ -108,4 +109,4 @@ split-debuginfo = "unpacked" | |||
[profile.release] | |||
lto = "thin" | |||
codegen-units = 16 | |||
overflow-checks = true | |||
overflow-checks = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No newline at EOF
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
// We specify the flags to include all pre-taproot and taproot | ||
// verifications explicitly. | ||
// https://github.com/rust-bitcoin/rust-bitcoinconsensus/blob/master/src/lib.rs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why include pre-taproot?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had tried it without and it fails without the other flags; it seems like there's some sort of roll-up. They have a method for getting the flags based on block height, where they additively enable each flag per activation block height. This is what they do in their tests, anyway.
/// The rotate-key frost verification signing round failed for the aggregate | ||
/// key. | ||
#[error("rotate-key frost verification signing failed for aggregate key: {0}")] | ||
DkgVerificationFailed(Box<PublicKeyXOnly>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was about to ask the same :)
.inspect_err(|error| { | ||
tracing::warn!(%error, "🔐 failed to assert that all signers have signed with the new aggregate key; aborting"); | ||
})?; | ||
tracing::info!("🔐 all signers have signed with the new aggregate key; proceeding"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WSTS
will only return OperationResult::SignTaproot
if the signature passes verification. But there's no harm in verifying it again, it only takes milliseconds.
signer/src/transaction_signer.rs
Outdated
@@ -714,7 +706,7 @@ where | |||
let (id, aggregate_key) = match msg.id { | |||
WstsMessageId::Dkg(_) => { | |||
tracing::warn!( | |||
"received nonce-request for DKG round, which is not supported" | |||
"🔐 received nonce-request for DKG round, which is not supported" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Putting emoji in logs still seems bizarre. Welcome to the future!
Description
Closes: #1300 (Alternative 3)
I'm still actively working on this but feel free to comment
Changes
Testing Information
Checklist: