Skip to content

Feat/unified secp256k1 k256#7305

Open
rob-stacks wants to merge 12 commits into
stacks-network:developfrom
rob-stacks:feat/unified_secp256k1_k256
Open

Feat/unified secp256k1 k256#7305
rob-stacks wants to merge 12 commits into
stacks-network:developfrom
rob-stacks:feat/unified_secp256k1_k256

Conversation

@rob-stacks

@rob-stacks rob-stacks commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Description

This patch unifies the secp256k1 curve api to use the k256 crate (https://crates.io/crates/k256)

Additional unit tests have been added to ensure the whole api is covered

Applicable issues

  • fixes #

Additional info (benefits, drawbacks, caveats)

Checklist

  • Test coverage for new or modified code paths
  • For new Clarity features or consensus changes, add property tests (see docs/property-testing.md)
  • Changelog fragment(s) or "no changelog" label added (see changelog.d/README.md)
  • Required documentation changes (e.g., rpc/openapi.yaml for RPC endpoints, event-dispatcher.md for new events)
  • New clarity functions have corresponding PR in clarity-benchmarking repo

@rob-stacks rob-stacks marked this pull request as ready for review June 15, 2026 13:51
@brice-stacks

Copy link
Copy Markdown
Contributor

I added @benjamin-stacks as a reviewer since he's just been making changes in here. I'll review after he does a pass. 🙏

@benjamin-stacks benjamin-stacks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just did an initial pass and pointed out a few things that we don't have to implement because they're already provided by those crates.

Will have a deeper look once develop with 30ee31d is merged into this branch.

Comment on lines +267 to +277
fn is_low_s(sig: &K256Signature) -> bool {
// secp256k1 group order n divided by 2 (big-endian)
const HALF_ORDER: [u8; 32] = [
0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x5d, 0x57, 0x6e, 0x73, 0x57, 0xa4, 0x50, 0x1d, 0xdf, 0xe9, 0x2f, 0x46, 0x68, 0x1b,
0x20, 0xa0,
];
// GenericArray<u8, U64> derefs to [u8]
let bytes = sig.to_bytes();
bytes[32..64] <= HALF_ORDER[..]
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn is_low_s(sig: &K256Signature) -> bool {
// secp256k1 group order n divided by 2 (big-endian)
const HALF_ORDER: [u8; 32] = [
0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x5d, 0x57, 0x6e, 0x73, 0x57, 0xa4, 0x50, 0x1d, 0xdf, 0xe9, 0x2f, 0x46, 0x68, 0x1b,
0x20, 0xa0,
];
// GenericArray<u8, U64> derefs to [u8]
let bytes = sig.to_bytes();
bytes[32..64] <= HALF_ORDER[..]
}
fn is_low_s(sig: &K256Signature) -> bool {
!bool::from(sig.s().is_high())
}

Comment on lines +545 to +560
fn negate_secp256k1_scalar(s: &[u8; 32]) -> [u8; 32] {
// secp256k1 group order n
const N: [u8; 32] = [
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C,
0xD0, 0x36, 0x41, 0x41,
];
let mut result = [0u8; 32];
let mut borrow: i32 = 0;
for i in (0..32).rev() {
let diff = N[i] as i32 - s[i] as i32 - borrow;
result[i] = diff.rem_euclid(256) as u8;
borrow = if diff < 0 { 1 } else { 0 };
}
result
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn negate_secp256k1_scalar(s: &[u8; 32]) -> [u8; 32] {
// secp256k1 group order n
const N: [u8; 32] = [
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C,
0xD0, 0x36, 0x41, 0x41,
];
let mut result = [0u8; 32];
let mut borrow: i32 = 0;
for i in (0..32).rev() {
let diff = N[i] as i32 - s[i] as i32 - borrow;
result[i] = diff.rem_euclid(256) as u8;
borrow = if diff < 0 { 1 } else { 0 };
}
result
}
fn negate_secp256k1_scalar(s: &[u8; 32]) -> [u8; 32] {
let s = NonZeroScalar::<Secp256k1>::try_from(&s[..])
.expect("s must be a non-zero value from the secp256k1 field");
s.negate().to_bytes().into()
}

Comment on lines +138 to +142
pub fn to_der_signature(&self) -> Option<Vec<u8>> {
let (sig, _) = self.to_secp256k1_recoverable()?;
let bytes: [u8; 64] = sig.to_bytes().into();
Some(secp256k1_der_encode(&bytes))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn to_der_signature(&self) -> Option<Vec<u8>> {
let (sig, _) = self.to_secp256k1_recoverable()?;
let bytes: [u8; 64] = sig.to_bytes().into();
Some(secp256k1_der_encode(&bytes))
}
pub fn to_der_signature(&self) -> Option<Vec<u8>> {
let (sig, _) = self.to_secp256k1_recoverable()?;
Some(sig.to_der().to_vec())
}

Since this functionality is provided by the ecdsa crate, we can avoid the custom implementation secp256k1_der_encode.

It looks like it was fun to implement though 😅 But I'd rather we reinvented as few wheels as possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants