-
Notifications
You must be signed in to change notification settings - Fork 41
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
Schnorr signature support #258
Conversation
This allows a more recent version of libsecp256k1 that has schnorr signatures.
@michaelpj and @iquerejeta - does this seem good? |
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.
Naming looks good. I've left some comments motivated by consistency. Note that I'm not a haskell expert. But if we decide to have some generic names (such as secpKeyPairCreate
or SECP256k1SecKey
), would it make sense to do qualified imports to make these types or functions explicitly linked to each algorithm?
type SizeVerKeyDSIGN SchnorrSecp256k1DSIGN = 64 | ||
type Signable SchnorrSecp256k1DSIGN = SignableRepresentation | ||
newtype VerKeyDSIGN SchnorrSecp256k1DSIGN = | ||
VerKeySchnorr256k1 (ForeignPtr SECP256k1XOnlyPubKey) |
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.
To maintain consistency, we may want to add
VerKeySchnorr256k1 (ForeignPtr SECP256k1XOnlyPubKey) | |
VerKeySchnorrSecp256k1 (ForeignPtr SECP256k1XOnlyPubKey) |
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.
Fair enough, I can do that.
Edit: On second thought, given that the constructors aren't publically-visible, what's the advantage here?
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.
Oh, the advantage is simply the consistency of the code. It is a nit, but after all we are making this PR to use correct naming. IMO we should be consistent, and maintain correctness of naming throughout the code.
type SizeVerKeyDSIGN EcdsaSecp256k1DSIGN = 64 | ||
type Signable EcdsaSecp256k1DSIGN = ((~) SECP.Msg) | ||
newtype VerKeyDSIGN EcdsaSecp256k1DSIGN = | ||
VerKeyEcdsaSecp256k1 SECP.PubKey |
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.
SECP
is still Crypto.Secp256k1
. Are these SECP
associated types and functions (e.g. SECP.PubKey
, SECP.derivePubKey
, SECP.signMsg
) here and below still exclusively linked to ECDSA?
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.
Yes, they are. I could rename the import if that would be clearer, but I figured it wouldn't matter much.
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.
The problem I see is that in the import
import qualified Crypto.Secp256k1 as SECP
there is no mention whatsoever to the actual signing algorithm. I would change the import to
import qualified Crypto.EcdsaSecp256k1 as SECP
or, if it is not possible, to
import qualified Crypto.Secp256k1 as ECDSA
SECP256k1XOnlyPubKey, | ||
secpKeyPairCreate, | ||
SECP256k1Context, | ||
secpKeyPairXOnlyPub, | ||
SECP256k1SecKey, | ||
secpSchnorrSigVerify, | ||
secpContextSignVerify, | ||
SECP256k1SchnorrSig, | ||
secpSchnorrSigSignCustom, | ||
secpContextCreate |
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.
Some reason why some of these functions contain Schnorr
, and other don't? or why some contain secp
and others SECP256k1
? And a nit, have we changed the order of the prefixes in purpose? In here we go secp256k1schnorr
while above we go with schnorrsecp256k1
.
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 because they mirror the naming of the C functions they wrap (without 256k1
everywhere, admittedly).
data SECP256k1KeyPair | ||
|
||
foreign import capi "secp256k1_extrakeys.h secp256k1_keypair_create" | ||
secpKeyPairCreate :: |
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.
are we excluding the prefix because the function is the same for both algorithms?
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 function is not algorithmically-tied. The secp256k1_extrakeys.h
header provides general keypair-related manipulation: it is not specific to Schnorr or ECDSA signatures.
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 am extremely unqualified to review the FFI stuff, we should find someone to do that.
{-# LANGUAGE DerivingVia #-} | ||
{-# LANGUAGE ScopedTypeVariables #-} | ||
|
||
module Cardano.Crypto.Schnorr ( |
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.
Hmmm, do you think we could contribute this upstream to secp256k1-haskell
?
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 could, but I chose not to, in the interests of getting this out the door faster. Possibly useful for the future though.
algorithmNameDSIGN _ = "schnorr-secp256k1" | ||
{-# NOINLINE deriveVerKeyDSIGN #-} | ||
deriveVerKeyDSIGN (SignKeySchnorr256k1 fp) = | ||
unsafeDupablePerformIO . withForeignPtr fp $ \skp -> do |
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.
Do we want to isolate this stuff in a module that provides a nicer Haskell interface, more like secp256k1-haskell
provides?
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 don't feel this would help much. When it comes to FFI stuff, my policy is to bind as closely as possible to the C API, then wrap it directly. Adding another level of indirection in between doesn't strike me as useful, and has actually backfired quite badly in the past.
deriveVerKeyDSIGN (SignKeySchnorr256k1 fp) = | ||
unsafeDupablePerformIO . withForeignPtr fp $ \skp -> do | ||
let skp' :: Ptr CUChar = castPtr skp | ||
allocaBytes 96 $ \kpp -> do |
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.
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 saw that, but I actually don't think it helps all that much. I prefer keeping close to C, magic numbers and all, and avoiding the cost of 'calling down' type-level numbers to the value level.
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.
Nonetheless, if it's going to be in this repository, I think it should follow the practices of this repository 🤷
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 respectfully disagree. You're injecting quite expensive code into (arguably) a hot code path for no real benefit, since nobody outside of the maintainers of this repo will see it. If magic numbers are a concern, I am happy to replace those with named constants.
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.
You're injecting quite expensive code into (arguably) a hot code path for no real benefit
I think both of these are wrong:
- The code is not expensive. GHC happily reifies the statically known information at compile time: I looked at the Core for the
Ed25519
module and it has numeric literals as the size arguments. - This is not a hot path: we are calling expensive primitives, we shouldn't need to worry too much about the FFI overhead.
In any case, please remove the magic numbers. You already had to define named constants in the class: use them. I also don't think there's a good reason not to use the library helpers here, and it makes things more consistent.
res <- secpKeyPairCreate ctxPtr kpp skp' | ||
when (res /= 1) (error "signDSIGN: Failed to create keypair") | ||
sigFP <- mallocForeignPtrBytes 64 | ||
let (msgFP, msgOff, msgLen) = toForeignPtr bs |
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 you could replace many (all?) the uses of toForeignPointer
with useAsCString(Len)
, which doesn't require the bytestring Internal
module.
let skp' :: Ptr CUChar = castPtr skp | ||
allocaBytes 96 $ \kpp -> do | ||
res <- secpKeyPairCreate ctxPtr kpp skp' | ||
when (res /= 1) (error "deriveVerKeyDSIGN: Failed to create keypair") |
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.
The error handling around here seems a bit strange, but I note that some of the other modules at least throw IO exceptions: https://github.com/input-output-hk/cardano-base/blob/39f9cf0eaf15a6888a47ac9d8de0b37dd1131d0b/cardano-crypto-class/src/Cardano/Crypto/DSIGN/Ed25519.hs#L53
Perhaps that function could be moved to Cardano.Crypto.Util
and used here too.
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'm not sure whether error
or an IO exception is more appropriate here. We're using unsafe*PerformIO
everywhere anyway, so I don't think it makes that much difference.
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.
error
throws an IOException
anyway, doesn't it?
OnlyCheckWhnfNamed "VerKeySchnorr256k1" (ForeignPtr SECP256k1XOnlyPubKey) | ||
) | ||
newtype SignKeyDSIGN SchnorrSecp256k1DSIGN = | ||
SignKeySchnorr256k1 (ForeignPtr SECP256k1SecKey) |
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 not use PinnedSizedBytes
for these?
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.
For the same reason as mentioned previously. My experience with FFI tells me that it's much better to be closer to C, and 'calling down' type level numbers to the value level is not a cheap operation.
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.
Again, this is false: look at the Core if you don't believe me. These are statically known, so there is no overhead.
All of my recent comments were just caused by taking a look at some of the other code in this package, and suggesting ways of making it more consistent and using the utilities that the package provides. It would be great if you could do such a pass yourself, you'll probably find more stuff that I haven't spotted! |
@michaelpj I have now rewritten the Schnorr signature work to use the wrappers in |
unsafePackCStringLen (castPtr p, 64) | ||
psb <- withForeignPtr (plusForeignPtr bsFP bsOff) $ \bsp -> do | ||
psbCreate $ \skp -> do | ||
copyPtr skp bsp 32 |
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 did this change? Please replace the magic numbers with constants.
I think the only outstanding comment is the one about magic numbers. I would still prefer you used @iquerejeta are you happy with the naming now? |
There are two unresolved comments above regarding naming. I agree with @kozross that the functions will not be exposed so we can be more flexible with the naming, but I still believe we need to be consistent and correct, for ease of maintenance of the code. The two particular cases are with the qualified import of ECDSA
where we cannot see that it actually relates to ECDSA. I would prefer something along the lines of And the second point is that |
@michaelpj How do you get a @iquerejeta I'm not sure what you're asking for here: |
Okay, LGTM. Please merge master and I'll merge this. |
Normally I feel like you get the option to merge master into other people's branches, but not here 🤔 |
Sounds good. I think that even if the naming confusion is upstream, we should make it as clear as we can in our code base. So that when future developers come to change the code, they know that they are handling the ECDSA algorithm, and not something different. My impression is that the upstream library was initially build only for ECDSA, and afterwards schnorr was added. That might justify some of the confusing naming. However, we are in a better position. We are including both ECDSA and Schnorr at the same time, so I don't see the need of making the same mistakes as upstream.
Sounds reasonable to me. I think it is important to be consistent. Edit: And sorry if I made some comments that did not make much sense with respect to renaming the modules. As I said, I've never programmed in Haskell, so it was hard for me to distinguish between the names being introduced here, and the names of upstream. |
@iquerejeta |
Follow ups in #262 |
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 to me.
@@ -1,129 +1,127 @@ | |||
cabal-version: 2.2 | |||
cabal-version: 2.2 |
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.
It would have been better to leave the formatting style intact; reformatting file like this when you're only making minor changes produces "diff noise", makes code harder to merge / rebase, and makes it more difficult to spot the actual changes. Improving formatting is great, but I'd recommend (for the future) to do it in a separate commit.
let skp' :: Ptr CUChar = castPtr skp | ||
allocaBytes 96 $ \kpp -> do | ||
res <- secpKeyPairCreate ctxPtr kpp skp' | ||
when (res /= 1) (error "deriveVerKeyDSIGN: Failed to create keypair") |
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.
error
throws an IOException
anyway, doesn't it?
This forms the first step in addressing this requirement, as well as addressing this naming concern. The test suite has been extended to cover this case as well.