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

Nakamoto: Validate stacker signature of a Nakamoto Block #4039

Merged
merged 26 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ba812f1
Update wsts and p256k1 libs to latest
jferrant Nov 8, 2023
dde02b3
Add wsts::common::Signature to and from SchnorrSignature conversion
jferrant Nov 8, 2023
382c258
Add a test for schnorr signature serde
jferrant Nov 8, 2023
54c2964
Add a stacker signature hash function that includes the miner signature
jferrant Nov 8, 2023
42d79c8
Change stacker_signature type to a SchnorrSignature
jferrant Nov 14, 2023
698db67
Update expects_stacker_signature to accept the block signature hash a…
jferrant Nov 14, 2023
0a83a33
Add get_aggregate_public_key_pox_4 function implementaton
jferrant Nov 14, 2023
7cc00a2
Retrieve aggregate public key in get_reward_set_nakamoto
jferrant Nov 14, 2023
591d93b
Add aggregate public key to reward set struct
jferrant Nov 14, 2023
f4d6f9c
Get the aggregate public key from the sortition db preprocessed rewar…
jferrant Nov 14, 2023
bbd17bd
Add get-aggregate-public-key to pox-4.clar and fix typo in contract call
jferrant Nov 14, 2023
096d04a
Replace stacker_signature with signer_signature
jferrant Nov 14, 2023
37c4e86
CRC: make sure to reattempt getting the reward cycle info if no DKG set
jferrant Nov 14, 2023
4dae061
Search for the first sortition of the prepare phase of the parent rew…
jferrant Nov 20, 2023
f57d9f8
Pox-4 activates with old block formats so update aggregate key even i…
jferrant Nov 20, 2023
0edc853
Add set-aggregate-public-key and fix point deserialization from pox-4
jferrant Nov 20, 2023
bb4f764
Boot nakamoto by simulating signer DKG rounds to set dkg in contract
jferrant Nov 20, 2023
2d12c07
Cleanup is_some check for bhh
jferrant Nov 20, 2023
b1a9fe2
Fix TestPeer to use the correct TestSigners
jferrant Nov 20, 2023
c4e897a
Only set the aggregate public key if in Pox 4
jferrant Nov 20, 2023
3b85478
Cleanup get_reard_cycle_aggregate_public_key and set aggregate key fo…
jferrant Nov 22, 2023
7630afe
BUG: fix find_prepare_phase_sortitions to not include one BEFORE prep…
jferrant Nov 22, 2023
5f4968e
Always retrieve the aggregate public key from pox-4
jferrant Nov 28, 2023
3377a9a
Update replay_reward_cycle to keep retrying failed blocks
jferrant Nov 28, 2023
dbea8db
CRC: add clarifying comments and names
jferrant Nov 29, 2023
6eeff19
CRC: add clarifying names and todo with issue 4109
jferrant Nov 30, 2023
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
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions stacks-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ slog-term = "2.6.0"
slog-json = { version = "2.3.0", optional = true }
chrono = "0.4.19"
libc = "0.2.82"
wsts = { workspace = true }

[target.'cfg(unix)'.dependencies]
nix = "0.23"
Expand Down Expand Up @@ -69,6 +70,7 @@ features = ["std"]
rstest = "0.11.0"
rstest_reuse = "0.1.3"
assert-json-diff = "1.0.0"
rand_core = "0.6"

[features]
default = ["developer-mode"]
Expand Down
6 changes: 5 additions & 1 deletion stacks-common/src/types/chainstate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use crate::codec::{read_next, write_next, Error as CodecError, StacksMessageCode
use crate::consts::{FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH};
use crate::deps_common::bitcoin::util::hash::Sha256dHash;
use crate::util::hash::{to_hex, DoubleSha256, Hash160, Sha512Trunc256Sum, HASH160_ENCODED_SIZE};
use crate::util::secp256k1::{MessageSignature, Secp256k1PrivateKey, Secp256k1PublicKey};
use crate::util::secp256k1::{
MessageSignature, SchnorrSignature, Secp256k1PrivateKey, Secp256k1PublicKey,
};
use crate::util::uint::Uint256;
use crate::util::vrf::{VRFProof, VRF_PROOF_ENCODED_SIZE};

Expand Down Expand Up @@ -336,6 +338,7 @@ impl_byte_array_rusqlite_only!(VRFProof);
impl_byte_array_rusqlite_only!(TrieHash);
impl_byte_array_rusqlite_only!(Sha512Trunc256Sum);
impl_byte_array_rusqlite_only!(MessageSignature);
impl_byte_array_rusqlite_only!(SchnorrSignature);

impl_byte_array_message_codec!(TrieHash, TRIEHASH_ENCODED_SIZE as u32);
impl_byte_array_message_codec!(Sha512Trunc256Sum, 32);
Expand All @@ -346,6 +349,7 @@ impl_byte_array_message_codec!(BurnchainHeaderHash, 32);
impl_byte_array_message_codec!(BlockHeaderHash, 32);
impl_byte_array_message_codec!(StacksBlockId, 32);
impl_byte_array_message_codec!(MessageSignature, 65);
impl_byte_array_message_codec!(SchnorrSignature, 65);

impl BlockHeaderHash {
pub fn to_hash160(&self) -> Hash160 {
Expand Down
121 changes: 120 additions & 1 deletion stacks-common/src/util/secp256k1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

use rand::{thread_rng, RngCore};
use secp256k1;
use secp256k1::ecdsa::{
Expand All @@ -27,8 +26,12 @@ use secp256k1::{
use serde::de::{Deserialize, Error as de_Error};
use serde::ser::Error as ser_Error;
use serde::Serialize;
use wsts::common::Signature as WSTSSignature;
use wsts::curve::point::{Compressed, Point};
use wsts::curve::scalar::Scalar;

use super::hash::Sha256Sum;
use crate::impl_byte_array_message_codec;
use crate::types::{PrivateKey, PublicKey};
use crate::util::hash::{hex_bytes, to_hex};

Expand Down Expand Up @@ -115,6 +118,45 @@ impl Default for Secp256k1PublicKey {
}
}

pub struct SchnorrSignature(pub [u8; 65]);
impl_array_newtype!(SchnorrSignature, u8, 65);
impl_array_hexstring_fmt!(SchnorrSignature);
impl_byte_array_newtype!(SchnorrSignature, u8, 65);
impl_byte_array_serde!(SchnorrSignature);
pub const SCHNORR_SIGNATURE_ENCODED_SIZE: u32 = 65;

impl Default for SchnorrSignature {
/// Creates a default Schnorr Signature. Note this is not a valid signature.
fn default() -> Self {
Self([0u8; 65])
}
}

impl SchnorrSignature {
/// Attempt to convert a Schnorr signature to a WSTS Signature
pub fn to_wsts_signature(&self) -> Option<WSTSSignature> {
// TODO: update wsts to add a TryFrom for a [u8; 65] and a slice to a Signature
let point_bytes: [u8; 33] = self.0[..33].try_into().ok()?;
let scalar_bytes: [u8; 32] = self.0[33..].try_into().ok()?;
let point = Point::try_from(&Compressed::from(point_bytes)).ok()?;
let scalar = Scalar::from(scalar_bytes);
Some(WSTSSignature {
R: point,
z: scalar,
})
}
}

/// Convert a WSTS Signature to a SchnorrSignature
impl From<&WSTSSignature> for SchnorrSignature {
fn from(signature: &WSTSSignature) -> Self {
let mut buf = [0u8; 65];
buf[..33].copy_from_slice(&signature.R.compress().data);
buf[33..].copy_from_slice(&signature.z.to_bytes());
SchnorrSignature(buf)
}
}

impl Secp256k1PublicKey {
#[cfg(any(test, feature = "testing"))]
pub fn new() -> Secp256k1PublicKey {
Expand Down Expand Up @@ -701,4 +743,81 @@ mod tests {
runtime_verify - runtime_recover
);
}

#[test]
fn test_schnorr_signature_serde() {
use wsts::traits::Aggregator;

// Test that an empty conversion fails.
let empty_signature = SchnorrSignature::default();
assert!(empty_signature.to_wsts_signature().is_none());

// Generate a random Signature and ensure it successfully converts
let mut rng = rand_core::OsRng::default();
let msg =
"You Idiots! These Are Not Them! You\'ve Captured Their Stunt Doubles!".as_bytes();
jferrant marked this conversation as resolved.
Show resolved Hide resolved

let num_keys = 10;
let threshold = 7;
let party_key_ids: Vec<Vec<u32>> =
vec![vec![0, 1, 2], vec![3, 4], vec![5, 6, 7], vec![8, 9]];
let num_parties = party_key_ids.len().try_into().unwrap();

// Create the parties
let mut signers: Vec<wsts::v2::Party> = party_key_ids
.iter()
.enumerate()
.map(|(pid, pkids)| {
wsts::v2::Party::new(
pid.try_into().unwrap(),
pkids,
num_parties,
num_keys,
threshold,
&mut rng,
)
})
.collect();

// Generate an aggregate public key
let comms = match wsts::v2::test_helpers::dkg(&mut signers, &mut rng) {
Ok(comms) => comms,
Err(secret_errors) => {
panic!("Got secret errors from DKG: {:?}", secret_errors);
}
};
let aggregate_public_key = comms
.iter()
.fold(Point::default(), |s, comm| s + comm.poly[0]);

// signers [0,1,3] have "threshold" keys
{
let mut signers = [signers[0].clone(), signers[1].clone(), signers[3].clone()].to_vec();
let mut sig_agg = wsts::v2::Aggregator::new(num_keys, threshold);

sig_agg.init(comms.clone()).expect("aggregator init failed");

let (nonces, sig_shares, key_ids) =
wsts::v2::test_helpers::sign(msg, &mut signers, &mut rng);
let original_signature = sig_agg
.sign(msg, &nonces, &sig_shares, &key_ids)
.expect("aggregator sig failed");
// Serialize the signature and verify the results
let schnorr_signature = SchnorrSignature::from(&original_signature);
assert_eq!(
schnorr_signature[..33],
original_signature.R.compress().data[..]
);
assert_eq!(schnorr_signature[33..], original_signature.z.to_bytes());

// Deserialize the signature and verify the results
let reverted_signature = schnorr_signature
.to_wsts_signature()
.expect("Failed to convert schnorr signature to wsts signature");
assert_eq!(reverted_signature.R, original_signature.R);
assert_eq!(reverted_signature.z, original_signature.z);
assert!(original_signature.verify(&aggregate_public_key, msg));
assert!(reverted_signature.verify(&aggregate_public_key, msg));
}
}
}
1 change: 1 addition & 0 deletions stackslib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pox-locking = { path = "../pox-locking" }
libstackerdb = { path = "../libstackerdb" }
siphasher = "0.3.7"
wsts = {workspace = true}
rand_core = "0.6"

[target.'cfg(unix)'.dependencies]
nix = "0.23"
Expand Down
32 changes: 16 additions & 16 deletions stackslib/src/chainstate/burn/db/sortdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ use stacks_common::util::hash::{hex_bytes, to_hex, Hash160, Sha512Trunc256Sum};
use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PublicKey};
use stacks_common::util::vrf::*;
use stacks_common::util::{get_epoch_time_secs, log};
use wsts::common::Signature as WSTSSignature;
use wsts::curve::point::{Compressed, Point};

use crate::burnchains::affirmation::{AffirmationMap, AffirmationMapEntry};
use crate::burnchains::bitcoin::BitcoinNetworkType;
Expand All @@ -65,6 +67,7 @@ use crate::chainstate::burn::{
use crate::chainstate::coordinator::{
Error as CoordinatorError, PoxAnchorBlockStatus, RewardCycleInfo,
};
use crate::chainstate::nakamoto::NakamotoBlockHeader;
use crate::chainstate::stacks::address::{PoxAddress, StacksAddressExtensions};
use crate::chainstate::stacks::boot::PoxStartCycleInfo;
use crate::chainstate::stacks::db::{StacksChainState, StacksHeaderInfo};
Expand All @@ -80,11 +83,11 @@ use crate::core::{
FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH, STACKS_EPOCH_MAX,
};
use crate::net::neighbors::MAX_NEIGHBOR_BLOCK_DELAY;
use crate::net::{Error as NetError, Error};
use crate::net::Error as NetError;
use crate::util_lib::db::{
db_mkdirs, opt_u64_to_sql, query_count, query_row, query_row_columns, query_row_panic,
query_rows, sql_pragma, tx_begin_immediate, tx_busy_handler, u64_to_sql, DBConn, DBTx,
Error as db_error, FromColumn, FromRow, IndexDBConn, IndexDBTx,
db_mkdirs, get_ancestor_block_hash, opt_u64_to_sql, query_count, query_row, query_row_columns,
query_row_panic, query_rows, sql_pragma, tx_begin_immediate, tx_busy_handler, u64_to_sql,
DBConn, DBTx, Error as db_error, FromColumn, FromRow, IndexDBConn, IndexDBTx,
};

const BLOCK_HEIGHT_MAX: u64 = ((1 as u64) << 63) - 1;
Expand Down Expand Up @@ -1914,18 +1917,20 @@ impl<'a> SortitionHandleConn<'a> {
}

/// Does the sortition db expect to receive blocks
/// signed by this stacker set?
/// signed by this signer set?
///
/// This only works if `consensus_hash` is within one reward cycle (2100 blocks) of the
/// sortition pointed to by this handle's sortiton tip. If it isn't, then this
/// method returns Ok(false). This is to prevent a DDoS vector whereby compromised stale
/// Stacker keys can be used to blast out lots of Nakamoto blocks that will be accepted
/// Signer keys can be used to blast out lots of Nakamoto blocks that will be accepted
/// but never processed. So, `consensus_hash` can be in the same reward cycle as
/// `self.context.chain_tip`, or the previous, but no earlier.
pub fn expects_stacker_signature(
pub fn expects_signer_signature(
&self,
consensus_hash: &ConsensusHash,
_stacker_signature: &MessageSignature,
signer_signature: &WSTSSignature,
message: &[u8],
aggregate_public_key: &Point,
) -> Result<bool, db_error> {
let sn = SortitionDB::get_block_snapshot(self, &self.context.chain_tip)?
.ok_or(db_error::NotFoundError)
Expand Down Expand Up @@ -1976,16 +1981,11 @@ impl<'a> SortitionHandleConn<'a> {
}

// is this consensus hash in this fork?
let Some(bhh) = SortitionDB::get_burnchain_header_hash_by_consensus(self, consensus_hash)?
else {
return Ok(false);
};
let Some(_sortition_id) = self.get_sortition_id_for_bhh(&bhh)? else {
if SortitionDB::get_burnchain_header_hash_by_consensus(self, consensus_hash)?.is_none() {
return Ok(false);
};
}

// TODO: query set of stacker signers in order to get the aggregate public key
Ok(true)
Ok(signer_signature.verify(aggregate_public_key, message))
}

pub fn get_reward_set_size_at(&self, sortition_id: &SortitionId) -> Result<u16, db_error> {
Expand Down
1 change: 0 additions & 1 deletion stackslib/src/chainstate/coordinator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,6 @@ pub fn get_reward_cycle_info<U: RewardSetProvider>(
let epoch_at_height = SortitionDB::get_stacks_epoch(sort_db.conn(), burn_height)?.expect(
&format!("FATAL: no epoch defined for burn height {}", burn_height),
);

let reward_cycle_info = if burnchain.is_reward_cycle_start(burn_height) {
let reward_cycle = burnchain
.block_height_to_reward_cycle(burn_height)
Expand Down
1 change: 0 additions & 1 deletion stackslib/src/chainstate/nakamoto/coordinator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,6 @@ pub fn get_nakamoto_reward_cycle_info<U: RewardSetProvider>(

let reward_set =
provider.get_reward_set(burn_height, chain_state, burnchain, sort_db, &block_id)?;

debug!(
"Stacks anchor block (ch {}) {} cycle {} is processed",
&anchor_block_header.consensus_hash, &block_id, reward_cycle
Expand Down
Loading