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

Schnorr signature support #258

Merged
merged 17 commits into from
Mar 2, 2022
Merged

Conversation

kozross
Copy link
Contributor

@kozross kozross commented Feb 21, 2022

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.

@kozross
Copy link
Contributor Author

kozross commented Feb 21, 2022

@michaelpj and @iquerejeta - does this seem good?

Copy link
Collaborator

@iquerejeta iquerejeta left a 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)
Copy link
Collaborator

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

Suggested change
VerKeySchnorr256k1 (ForeignPtr SECP256k1XOnlyPubKey)
VerKeySchnorrSecp256k1 (ForeignPtr SECP256k1XOnlyPubKey)

Copy link
Contributor Author

@kozross kozross Feb 22, 2022

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?

Copy link
Collaborator

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
Copy link
Collaborator

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?

Copy link
Contributor Author

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.

Copy link
Collaborator

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

Comment on lines 45 to 54
SECP256k1XOnlyPubKey,
secpKeyPairCreate,
SECP256k1Context,
secpKeyPairXOnlyPub,
SECP256k1SecKey,
secpSchnorrSigVerify,
secpContextSignVerify,
SECP256k1SchnorrSig,
secpSchnorrSigSignCustom,
secpContextCreate
Copy link
Collaborator

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.

Copy link
Contributor Author

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 ::
Copy link
Collaborator

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?

Copy link
Contributor Author

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.

Copy link
Contributor

@michaelpj michaelpj left a 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 (
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

Choose a reason for hiding this comment

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

I note that this package has some utilities for slightly safer allocation, in here. I think you can use this in your foreign declarations (like here) to get some more safety and fewer magic numbers.

Copy link
Contributor Author

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.

Copy link
Contributor

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 🤷

Copy link
Contributor Author

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.

Copy link
Contributor

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
Copy link
Contributor

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")
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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)
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@michaelpj
Copy link
Contributor

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!

@kozross
Copy link
Contributor Author

kozross commented Feb 28, 2022

@michaelpj I have now rewritten the Schnorr signature work to use the wrappers in cardano-base, as well as useAsCStringLen. I believe this should resolve all outstanding concerns.

unsafePackCStringLen (castPtr p, 64)
psb <- withForeignPtr (plusForeignPtr bsFP bsOff) $ \bsp -> do
psbCreate $ \skp -> do
copyPtr skp bsp 32
Copy link
Contributor

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.

@michaelpj
Copy link
Contributor

I think the only outstanding comment is the one about magic numbers. I would still prefer you used SizedPtr, but I can do that afterwards so long as it's actually clear what the numbers mean.

@iquerejeta are you happy with the naming now?

@iquerejeta
Copy link
Collaborator

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

import qualified Crypto.Secp256k1 as SECP

where we cannot see that it actually relates to ECDSA. I would prefer something along the lines of import qualified Crypto.EcdsaSecp256k1, but I would be ok with the second suggestion.

And the second point is that VerKeySchnorr256k1 should be changed to VerKeySchnorrSecp256k1.

@kozross
Copy link
Contributor Author

kozross commented Feb 28, 2022

@michaelpj How do you get a SizedPtr to the contents of an arbitrary ByteString?

@iquerejeta I'm not sure what you're asking for here: Crypto.Secp256k1 refers specifically to this module here. I can import it as ECDSA, but to be honest, if the naming confusion is anywhere, it's upstream. I can rename VerkeySchnorr256k1 as you suggest, but then, for consistency, I would need to rename all the other constructors too.

@michaelpj
Copy link
Contributor

Okay, LGTM. Please merge master and I'll merge this.

@michaelpj
Copy link
Contributor

Normally I feel like you get the option to merge master into other people's branches, but not here 🤔

@iquerejeta
Copy link
Collaborator

iquerejeta commented Mar 1, 2022

I can import it as ECDSA, but to be honest, if the naming confusion is anywhere, it's upstream.

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.

I can rename VerkeySchnorr256k1 as you suggest, but then, for consistency, I would need to rename all the other constructors too.

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.

@kozross
Copy link
Contributor Author

kozross commented Mar 1, 2022

@iquerejeta secp256k1-haskell doesn't support Schnorr signatures at all, which is why I had to FFI into secp256k1 directly.

@michaelpj michaelpj merged commit 1587462 into IntersectMBO:master Mar 2, 2022
@michaelpj
Copy link
Contributor

Follow ups in #262

Copy link
Contributor

@tdammers tdammers left a 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
Copy link
Contributor

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")
Copy link
Contributor

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?

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.

4 participants